10

精读《pipe operator for JavaScript》

 2 years ago
source link: https://www.fly63.com/article/detial/11099
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Pipe Operator (|>) for JavaScript 提案给 js 增加了 Pipe 语法,这次结合 A pipe operator for JavaScript: introduction and use cases 文章一起深入了解这个提案。

Pipe 语法可以将函数调用按顺序打平。如下方函数,存在三层嵌套,但我们解读时需要由内而外阅读,因为调用顺序是由内而外的:

const y = h(g(f(x)))

Pipe 可以将其转化为正常顺序:

const y = x |> f(%) |> g(%) |> h(%)

Pipe 语法有两种风格,分别来自 Microsoft 的 F#) 与 Facebook 的 Hack)。

之所以介绍这两个,是因为 js 提案首先要决定 “借鉴” 哪种风格。js 提案最终采用了 Hack 风格,因此我们最好把 F# 与 Hack 的风格都了解一下,并对其优劣做一个对比,才能知其所以然。

Hack Pipe 语法

Hack 语法相对冗余,在 Pipe 时使用 % 传递结果:

'123.45' |> Number(%)

这个 % 可以用在任何地方,基本上原生 js 语法都支持:

value |> someFunction(1, %, 3) // function calls
value |> %.someMethod() // method call
value |> % + 1 // operator
value |> [%, 'b', 'c'] // Array literal
value |> {someProp: %} // object literal
value |> await % // awaiting a Promise
value |> (yield %) // yielding a generator value

F# Pipe 语法

F# 语法相对精简,默认不使用额外符号:

'123.45' |> Number

但在需要显式声明参数时,为了解决上一个 Pipe 结果符号从哪来的问题,写起来反而更为复杂:

2 |> $ => add2(1, $)

await 关键字 - Hack 优

F# 在 await yield 时需要特殊语法支持,而 Hack 可以自然的使用 js 内置关键字。

// Hack
value |> await %
// F#
value |> await

F# 代码看上去很精简,但实际上付出了高昂的代价 - await 是一个仅在 Pipe 语法存在的关键字,而非普通 await 关键字。如果不作为关键字处理,执行逻辑就变成了 await(value) 而不是 await value。

解构 - F# 优

正因为 F# 繁琐的变量声明,反而使得在应对解构场景时得心应手:

// F#
value |> ({ a, b }) => someFunction(a, b)
// Hack
value |> someFunction(%.a, %.b)

Hack 也不是没有解构手段,只是比较繁琐。要么使用立即调用函数表达式 IIFE:

value |> (({ a, b }) => someFunction(a, b))(%)

要么使用 do 关键字:

value |> do { const { a, b } = %; someFunction(a, b) }

但 Hack 虽败犹荣,因为解决方法都使用了 js 原生提供的语法,所以反而体现出与 js 已有生态亲和性更强,而 F# 之所以能优雅解决,全都归功于自创的语法,这些语法虽然甜,但割裂了 js 生态,这是 F# like 提案被放弃的重要原因之一。

潜在改进方案

虽然选择了 Hack 风格,但 F# 与 Hack 各有优劣,所以列了几点优化方案。

利用 Partial Application Syntax 提案降低 F# 传参复杂度

F# 被诟病的一个原因是传参不如 Hack 简单:

// Hack
2 |> add2(1, %)
// F#
2 |> $ => add2(1, $)

但如果利用处于 stage1 的提案 Partial Application Syntax 可以很好的解决问题。

这里就要做一个小插曲了。js 对柯里化没有原生支持,但 Partial Application Syntax 提案解决了这个问题,语法如下:

const add = (x, y) => x + y;
const addOne = add~(1, ?);
addOne(2); // 3

即利用 fn~(?, arg) 的语法,将任意函数柯里化。这个特性解决 F# 传参复杂问题简直绝配,因为 F# 的每一个 Pipe 都要求是一个函数,我们可以将要传参的地方记为 ?,这样返回值还是一个函数,完美符合 F# 的语法:

// F#
2 |> add~(1, ?)

上面的例子拆开看就是:

const addOne = add~(1, ?)
2 |> addOne

想法很美好,但 Partial Application Syntax 得先落地。

融合 F# 与 Hack 语法

在简单情况下使用 F#,需要利用 % 传参时使用 Hack 语法,两者混合在一起写就是:

const resultArray = inputArray
  |> filter(%, str => str.length >= 0) // Hack
  |> map(%, str => '['+str+']') // Hack
  |> console.log // F#

不过这个 提案 被废弃了。

创造一个新的操作符

如果用 |> 表示 Hack 语法,用 |>> 表示 F# 语法呢?

const resultArray = inputArray
  |> filter(%, str => str.length >= 0) // Hack
  |> map(%, str => '['+str+']') // Hack
  |>> console.log // F#

也是看上去很美好,但这个特性连提案都还没有。

如何用现有语法模拟 Pipe

即便没有 Pipe Operator (|>) for JavaScript 提案,也可以利用 js 现有语法模拟 Pipe 效果,以下是几种方案。

Function.pipe()

利用自定义函数构造 pipe 方法,该语法与 F# 比较像:

const resultSet = Function.pipe(
  inputSet,
  $ => filter($, x => x >= 0)
  $ => map($, x => x * 2)
  $ => new Set($)
)

缺点是不支持 await,且存在额外函数调用。

使用中间变量

说白了就是把 Pipe 过程拆开,一步步来写:

const filtered = filter(inputSet, x => x >= 0)
const mapped = map(filtered, x => x * 2)
const resultSet = new Set(mapped)

没什么大问题,就是比较冗余,本来可能一行能解决的问题变成了三行,而且还声明了三个中间变量。

改造一下,将中间变量变成复用的:

let $ = inputSet
$ = filter($, x => x >= 0)
$ = map($, x => x * 2)
const resultSet = new Set($)

这样做可能存在变量污染,可使用 IIFE 解决。

Pipe Operator 语义价值非常明显,甚至可以改变编程的思维方式,在串行处理数据时非常重要,因此命令行场景非常常见,如:

cat "somefile.txt" | echo

因为命令行就是典型的输入输出场景,而且大部分都是单输入、单输出。

在普通代码场景,特别是处理数据时也需要这个特性,大部分具有抽象思维的代码都进行了各种类型的管道抽象,比如:

const newValue = pipe(
  value,
  doSomething1,
  doSomething2,
  doSomething3
)

如果 Pipe Operator (|>) for JavaScript 提案通过,我们就不需要任何库实现 pipe 动作,可以直接写成:

const newValue = value |> doSomething1(%) |> doSomething2(%) |> doSomething3(%)

这等价于:

const newValue = doSomething3(doSomething2(doSomething1(value)))

显然,利用 pipe 特性书写处理流程更为直观,执行逻辑与阅读逻辑是一致的。

实现 pipe 函数

即便没有 Pipe Operator (|>) for JavaScript 提案,我们也可以一行实现 pipe 函数:

const pipe = (...args) => args.reduce((acc, el) => el(acc))

但要实现 Hack 参数风格是不可能的,顶多实现 F# 参数风格。

js 实现 pipe 语法的考虑

从 提案 记录来看,F# 失败有三个原因:

  • 内存性能问题。
  • await 特殊语法。
  • 割裂 js 生态。

其中割裂 js 生态是指因 F# 语法的特殊性,如果有太多库按照其语法实现功能,可能导致无法被非 Pipe 语法场景所复用。

甚至还有部分成员反对 隐性编程(Tacit programming),以及柯里化提案 Partial Application Syntax,这些会使 js 支持的编程风格与现在差异过大。

看来处于鄙视链顶端的编程风格在 js 是否支持不是能不能的问题,而是想不想的问题。

pipe 语法的弊端

下面是普通 setState 语法:

setState(state => ({
  ...state,
  value: 123
}))

如果改为 immer 写法如下:

setState(produce(draft => draft.value = 123))

得益于 ts 类型自动推导,在内层 produce 里就已经知道 value 是字符串类型,此时如果输入字符串会报错,而如果其在另一个上下文的 setState 内,类型也会随着上下文的变化而变化。

但如果写成 pipe 模式:

produce(draft => draft.value = 123) |> setState

因为先考虑的是如何修改数据,此时还不知道后面的 pipe 流程是什么,所以 draft 的类型无法确定。所以 pipe 语法仅适用于固定类型的数据处理流程。

pipe 直译为管道,潜在含义是 “数据像流水线一样被处理”,也可以形象理解为每个函数就是一个不同的管道,显然下一个管道要处理上一个管道的数据,并将结果输出到下一个管道作为输入。

合适的管道数量与体积决定了一条生产线是否高效,过多的管道类型反而会使流水线零散而杂乱,过少的管道会让流水线笨重不易拓展,这是工作中最大的考验。

讨论地址是:精读《pipe operator for JavaScript》· Issue #395 · dt-fe/weekly
关注 前端精读微信公众号

站长推荐

1.云服务推荐: 国内主流云服务商/虚拟空间,各类云产品的最新活动,优惠券领取。领取地址:阿里云腾讯云硅云

链接: https://www.fly63.com/article/detial/11099


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK