tl;dr: 区别在于-> 管道到第一个参数,而|> 管道到最后一个参数。那就是:
x -> f(y, z) <=> f(x, y, z)
x |> f(y, z) <=> f(y, z, x)
不幸的是,有一些微妙之处和含义使这在实践中变得更加复杂和混乱。请耐心等待我尝试解释其背后的历史。
管龄前
在出现任何管道运算符之前,大多数函数式程序员设计的大多数函数都使用函数操作的“对象”作为最后一个参数。这是因为使用偏函数应用使函数组合变得更容易,而如果未应用的参数位于末尾,则在柯里化语言中偏函数应用变得更加容易。
柯里化
在柯里化语言中,每个函数都只接受一个参数。看起来接受两个参数的函数实际上是一个接受一个参数的函数,然后返回另一个接受另一个参数的函数,然后返回实际结果。因此这些是等价的:
let add = (x, y) => x + y
let add = x => y => x + y
或者更确切地说,第一种形式只是第二种形式的语法糖。
部分函数应用
这也意味着我们可以通过提供第一个参数轻松地部分应用一个函数,这将让它返回一个在产生结果之前接受第二个参数的函数:
let add3 = add(3)
let result = add3(4) /* result == 7 */
如果不使用柯里化,我们必须将其包装在一个函数中,这更加麻烦:
let add3 = y => add(3, y)
巧妙的功能设计
现在事实证明,大多数函数都在“主”参数上运行,我们可以将其称为函数的“对象”。 List 函数通常在特定列表上运行,例如,不能同时运行多个(当然,这也会发生)。因此,将主要参数放在最后可以让您更轻松地组合函数。例如,对于几个设计良好的函数,定义一个函数将可选值列表转换为具有默认值的实际值列表非常简单:
let values = default => List.map(Option.defaultValue(default)))
虽然使用“对象”设计的函数首先需要您编写:
let values = (list, default) =>
List.map(list, value => Option.defaultValue(value, default)))
管道时代的曙光(具有讽刺意味的是,这不是管道先行的)
据我了解,有人在 F# 中玩耍时发现了一种常见的管道模式,并认为为中间值提出命名绑定或使用太多该死的括号以倒序嵌套函数调用很麻烦。所以他发明了管道转发运算符|>。有了这个,管道可以写成
let result = list |> List.map(...) |> List.filter(...)
而不是
let result = List.filter(..., List.map(..., list))
或
let mappedList = List.map(..., list)
let result = List.filter(..., mapped)
但这仅在主要参数在最后一个时才有效,因为它依赖于通过柯里化的部分函数应用。
然后... BuckleScript
然后是 Bob,他首先编写了 BuckleScript,以便将 OCaml 代码编译为 JavaScript。 BuckleScript 被 Reason 采用,然后 Bob 继续为 BuckleScript 创建一个名为 Belt 的标准库。 Belt 忽略了我上面解释的几乎所有内容,将主要参数放在 前面。为什么?这还有待解释,但据我所知,这主要是因为 JavaScript 开发人员更熟悉它1。
不过,Bob 确实认识到管道运算符的重要性,因此他创建了自己的管道优先运算符 |.,它仅适用于 BuckleScript2。然后 Reason 开发人员认为这看起来有点丑陋且缺乏方向,所以他们想出了 -> 运算符,它转换为 |. 并且工作方式完全一样......除了它有不同的优先级,因此没有其他任何东西都不好玩。3
结论
管道优先运算符本身并不是一个坏主意。但是它在 BuckleScript 和 Reason 中实现和执行的方式引起了很多混乱。它有意想不到的行为,鼓励糟糕的函数设计,除非你全力以赴4,当根据你调用的函数类型在不同的管道操作符之间切换时会产生沉重的认知负担。
因此,我建议避免使用管道优先运算符(-> 或 |.),而是使用管道转发(|>)和 placeholder argument(也是 Reason 独有)如果您需要管道到“对象”优先功能,例如list |> List.map(...) |> Belt.List.keep(_, ...).
1这与类型推断的交互方式也有一些细微的差异,因为类型是从左到右推断的,但这对两种风格的 IMO 都没有明显的好处。
2 因为它需要句法转换。与 pipe-forward 不同,它不能仅作为普通运算符实现。
3 比如list |> List.map(...) -> Belt.List.keep(...)doesn't work as you'd expect
4 这意味着几乎无法使用在 pipe-first 运算符存在之前创建的所有库,因为这些库当然是在考虑原始 pipe-forward 运算符的情况下创建的。这实际上将生态系统一分为二。