75team / tc39

360 Ecma-TC39 工作组
89 stars 4 forks source link

【提案讨论】extensions(bind op)和 pipeline op 的对比 #9

Closed hax closed 4 years ago

hax commented 4 years ago

现在 pipe operator 和 bind operator 相比,在使用上的话,不再需要 this,而且还可以兼容现在像 lodash 这种扩展库,看起来更加有前景

首先如果看 underscore/lodash 的历史,可以发现其早期其实是以 this 方式运作的,如 _(a).first()。之所以后来改用所谓fp形式(_.first(a)),并不是因为大家真的想用fp [1],而是wrapper方式有很多难解的缺陷(比如性能,比如unwrap需要打断链式调用)。但换了所谓fp形式之后,也有缺陷,就是压根没法链式调用了。所以大家希望要上 pipe op。

[1] 至少当初不是的,或者说得难听点,很多人其实是叶公好龙,并不是想用真fp——比方说ramda这样的库其实大多数人是接受不了的。

从某种程度上说,underscore/lodash 之所以出现 fp 形式是因为 bind op 提案停滞了,大家迫不得已找其他出路,但也有问题,于是大家又想着解决这个问题,就推 pipe op,但是忘记了(或者否定了)其实本来 bind op 就可以解决问题。

bind op为什么会停滞,当然是因为 bind op 本身有一些问题,这也是为什么我要重新设计 extensions proposal 来取代它。这里暂不展开。

但其实pipe op一样问题严重,在 stage 1 上停滞不前。bind op虽然是stage 0,但当时的stage 0其实和stage 1比较类似,尤其在生态上也是有 babel 支持的。

我并不是想否定pipe和fp。但我认为,相对来说,extensions 是和 js 语言的现有特性合作得更好的,且 pipe(和其他一些fp需求)是可以建立在 extensions 之上的,比如我们其实不一定需要 x |> f,可以用 x::pipe(f),也就是 pipe 完全可以是不需要语法,而用 userland 库来解决。即使真要标准化,也可以只标准化库,这给到委员会更大的空间和自由,更小的压力。

下面我简要阐述下 pipe 和 extensions/bind 的区别:

  1. extensions/bind 毫无争议其左侧是作为 this 参数的。而 pipe 是有争议的,到底左侧是作为 this (比如有人可能想写 v |> Object.prototype.toString)还是唯一一个参数(常见的 F# style),还是第一个参数(Elixer style,允许直接用现有的lodash库 document.all |> lodash.map(e => ...) ),或者最后一个参数(比如 map 函数可能并不是像 lodash 那样签名为 (Iterable<T>, T => U) => U,而是和许多函数式语言一样顺序的 (T => U, Iterable<T>) => U,或者任意位置(使用 # topic 的smart style —— x |> f(1, #))。无论pipe op语法最后选择哪一个语义都会造成另一部分人的惊讶(比如 let m = o.m; x |> m(1) 到底表示什么),并让那些use cases变得相对写起来不爽。

  2. pipe 的运算符优先级和结合性是比较低的,比如 x |> o.f() 肯定会被理解为 x |> (o.f()) 而不是 (x |> o).f(),而且 pipe 调用是没有括号的(如 x |> f),因此和既有的OO风格的api(所有JS标准库都是OO风格的)合用会比较别扭,特别是已经被设计为 Fluent interface 的 api,如 (x.a().b() |> c |> d).a().b(),注意括号的不一致性严重破坏了流畅感。而 extensions/bind 其运算符优先级和结合性应该和 . 一致,这样就是 x.a().b()::c()::d().a().b() 。【注意当前 bind op的::优先级比 . 要低,也就是采用了靠近 pipe op 的优先级,这正是 bind op 草案的一大缺陷,也是造成不少负反馈的原因之一。】

  3. extensions/bind 应该可以比 pipe op 更简单的就地复用现有的 JS api。比如按照我的 extensions 草案,你可以写:

    
    const nullProto = Object.create(null)
    ...
    // 支持解构语法
    const Object::{hasOwnProperty, isPrototypeOf} = Object.prototype

for (const k in obj) { if (obj::hasOwnProperty(k) && nullProto::isPrototypeOf(obj[k]) ) { ... } }


而用 pipe 的话,就麻烦点:

```js
const nullProto = Object.create(null)
...

// 假设采用 F# style 的 pipe op
const hasOwnProperty = k => thisArg => Object.prototype.hasOwnProperty.call(thisArg, k)
const isPrototypeOf = v => thisArg => Object.prototype.isPrototypeOf.call(thisArg, v)

for (const k in obj) {
  if ((obj |> hasOwnProperty(k))
    && (nullProto |> isPrototypeOf(obj[k]))
  ) {
    ...
  }
}
  1. 在上面这个例子里我们也可以看到, F# style 的 pipe op 会产生中间函数,默认性能会较差。虽然不是不能进行内联优化,但本质上说最终可能需要通用性的针对fp进行优化,成本高,不是所有引擎都可以做(比如嵌入式引擎)。

以上。想到再补充。

hax commented 4 years ago

讨论移步 https://github.com/JSCIG/es-discuss/issues/3