tl;dr:决定性的区别在于->
管道到第一个参数,同时|>
管道到最后。那是:
x -> f(y, z) <=> f(x, y, z)
x |> f(y, z) <=> f(y, z, x)
不幸的是,有一些微妙之处和含义使得这在实践中变得更加复杂和混乱。请耐心听我解释其背后的历史。
管道时代之前
在出现任何管道运算符之前,大多数函数式程序员都将函数操作的“对象”作为最后一个参数来设计大多数函数。这是因为使用部分函数应用可以使函数组合变得更加容易,并且如果未应用的参数位于末尾,则在柯里化语言中,部分函数应用会变得更加容易。
Currying
在柯里化语言中,每个函数都只接受一个参数。看似接受两个参数的函数实际上是一个接受一个参数的函数,但随后返回另一个接受另一个参数的函数,进而返回实际结果。因此这些是等价的:
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))
or
let mappedList = List.map(..., list)
let result = List.filter(..., mapped)
但这仅在主要参数位于最后时才有效,因为它依赖于通过柯里化的部分函数应用。
然后...BuckleScript
Then along comes Bob, who first authored BuckleScript in order to compile OCaml code to JavaScript. BuckleScript was adopted by Reason, and then Bob went on to create a standard library for BuckleScript called Belt
. Belt
ignores almost everything I've explained above by putting the main argument first. Why? That has yet to be explained, but from what I can gather it's primarily because it's more familiar to JavaScript developers1.
Bob did recognize the importance of the pipe operator, however, so he created his own pipe-first operator, |.
, which works only with BuckleScript2. And then the Reason developers thought that looked a bit ugly and lacking direction, so they came up with the ->
operator, which translates to |.
and works exactly like it... except it has a different precedence and therefore doesn't play nice with anything else.3
结论
A pipe-first operator isn't a bad idea in itself. But the way it has been implemented and executed in BuckleScript and Reason invites a lot of confusion. It has unexpected behavior, encourages bad function design and unless one goes all in on it4, imposes a heavy cognitive tax when switching between the different pipe operators depending on what kind of function you're calling.
因此,我建议避免使用管道优先运算符(->
or |.
) 并使用管道转发 (|>
)与一个占位符参数 https://reasonml.github.io/docs/en/pipe-first#pipe-placeholders(也是 Reason 独有的)如果您需要通过管道传输到“对象”优先的函数,例如list |> List.map(...) |> Belt.List.keep(_, ...)
.
1 There are also some subtle differences with how this interacts with type inference, because types are inferred left-to-right, but it's not a clear benefit to either style IMO.
2 Because it requires syntactic transformation. It can't be implemented as just an ordinary operator, unlike pipe-forward.
3 For example, list |> List.map(...) -> Belt.List.keep(...)
doesn't work as you'd expect https://stackoverflow.com/q/54957566/7943564
4 Which means being unable to use almost every library created before the pipe-first operator existed, because those were of course created with the original pipe-forward operator in mind. This effectively splits the ecosystem in two.