在ReasonML中,->和|>有什么区别?

18
一段紧张的谷歌搜索给我提供了一些例子,人们在代码中同时使用这两种运算符,但通常它们看起来就像是做同一件事情的两种方式,甚至具有相同的名称。

1
两者之间有显著的差异,但这些差异并不是一眼就能看出来的。Javier Chávarri进行了一次全面的比较: https://www.javierchavarri.com/data-first-and-data-last-a-comparison/ - javinor
2个回答

35

简短概述:区别在于->管道传递到第一个参数,而|>管道传递到最后一个参数。即:

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。 Reason采用了BuckleScript,然后Bob继续为BuckleScript创建了一个标准库称为Belt。Belt通过将主参数放在前面而忽略了我解释的几乎所有内容。为什么呢?尚未解释,但从我所了解的来看,主要是因为对JavaScript开发人员更加熟悉。
然而,Bob确实认识到了管道运算符的重要性,所以他创建了自己的管道优先运算符|.,该运算符仅适用于BuckleScript。然后Reason开发人员认为它看起来有点丑陋且缺乏方向,因此他们想出了->运算符,它转换为|.并且完全像它一样...除了具有不同的优先级,因此与其他任何内容都不兼容。
结论
管道优先运算符本身并不是一个坏主意。但是,在BuckleScript和Reason中实现和执行它的方式会引起很多混淆。它具有意外的行为,鼓励糟糕的函数设计,并且除非人们完全投入其中,否则在调用不同类型的函数时切换不同的管道运算符会带来沉重的认知负担。
因此,我建议避免使用管道优先操作符(->|.),而是使用管道前进操作符(|>)并使用占位符参数(也仅适用于Reason),如果您需要将其导向“对象”优先函数,例如list |> List.map(...) |> Belt.List.keep(_, ...)

1这也与类型推断的交互方式有一些微妙的差异,因为类型是从左到右推断的,但在我看来,这对任何一种风格都没有明显的好处。

2因为它需要语法转换。它不能像管道前向操作符那样只实现为一个普通的运算符。

3例如,list |> List.map(...) -> Belt.List.keep(...) 不像你期望的那样工作

4这意味着无法使用几乎在管道前向操作符存在之前创建的所有库,因为当然是针对原始的管道前向操作符创建的。这有效地将生态系统分成两个部分。


5
如果他们使用了标记参数,那么BuckleScript就不需要单独的运算符。因为标记参数可以按任意顺序应用,包括在未标记的参数之前或之后。这将使他们保持t作为类型推断的第一位,同时仍然可以使用标准的 |> 运算符。Base 巧妙地使用了这个模式(例如,请参阅List,其中将函数 map 标记为 ~f)。 - Kevin Ji
@kevinji 确实,这是一个很好的观点,并且在这个过程中早早地被提出了很多次。不幸的是,Bob只是因为他个人不喜欢它而忽略了它。 - glennsl
另一个反对 -> 的论点是,它似乎会破坏我所拥有的任何版本的 refmt。当遇到 -> 时,它会显示语法错误。 - MCH
个人而言,我更喜欢使用|>管道操作符,但是显然re-script已经弃用了该操作符。假设re-script将是bucklescript/reasonml的未来,那么任何想要使用bs/rescript的人都需要习惯使用->管道操作符。 - masoodahm
我怀疑它实际上不会被删除,因为这将破坏OCaml的兼容性以及与大量库的向后兼容性。但即使它被删除了,在用户空间中重新添加也是微不足道的。 - glennsl

11

|>通常被称为“管道正向”。它是在更广泛的OCaml社区中使用的辅助功能,不仅限于ReasonML。它将左侧参数“注入”到右侧函数中作为最后一个参数:

0 |> f       == f(0)
0 |> g(1)    == g(1, 0)
0 |> h(1, 2) == h(1, 2, 0)
// and so on
-> 被称为“管道优先”,它是一种新的语法糖,将左侧参数注入到右侧函数或数据构造函数的第一个参数位置中:
0 -> f       == f(0)
0 -> g(1)    == g(0, 1)
0 -> h(1, 2) == h(0, 1, 2)
0 -> Some    == Some(0)

注意:->仅适用于BuckleScript编译成JavaScript时,对于本地编译不可用,因此不具有可移植性。更多详情请参见:https://reasonml.github.io/docs/en/pipe-first


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接