【问题标题】:What's the difference between -> and |> in reasonml?reasonml 中的 -> 和 |> 有什么区别?
【发布时间】:2019-08-23 18:27:12
【问题描述】:

一段时间的激烈谷歌搜索为我提供了一些示例,其中人们在一个代码中使用两种类型的运算符,但通常它们看起来就像做一件事的两种方式,它们甚至具有相同的名称

【问题讨论】:

标签: ocaml pipeline reason bucklescript


【解决方案1】:

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# 中玩耍时发现了一种常见的管道模式,并认为为中间值提出命名绑定或使用太多该死的括号以倒序嵌套函数调用很麻烦。所以他发明了管道转发运算符|&gt;。有了这个,管道可以写成

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 开发人员认为这看起来有点丑陋且缺乏方向,所以他们想出了 -&gt; 运算符,它转换为 |. 并且工作方式完全一样......除了它有不同的优先级,因此没有其他任何东西都不好玩。3

结论

管道优先运算符本身并不是一个坏主意。但是它在 BuckleScript 和 Reason 中实现和执行的方式引起了很多混乱。它有意想不到的行为,鼓励糟糕的函数设计,除非你全力以赴4,当根据你调用的函数类型在不同的管道操作符之间切换时会产生沉重的认知负担。

因此,我建议避免使用管道优先运算符(-&gt;|.),而是使用管道转发(|&gt;)和 placeholder argument(也是 Reason 独有)如果您需要管道到“对象”优先功能,例如list |&gt; List.map(...) |&gt; Belt.List.keep(_, ...).


1这与类型推断的交互方式也有一些细微的差异,因为类型是从左到右推断的,但这对两种风格的 IMO 都没有明显的好处。

2 因为它需要句法转换。与 pipe-forward 不同,它不能仅作为普通运算符实现。

3 比如list |&gt; List.map(...) -&gt; Belt.List.keep(...)doesn't work as you'd expect

4 这意味着几乎无法使用在 pipe-first 运算符存在之前创建的所有库,因为这些库当然是在考虑原始 pipe-forward 运算符的情况下创建的。这实际上将生态系统一分为二。

【讨论】:

  • 如果 BuckleScript 只使用 labeled arguments,则可以避免使用单独的运算符,因为标记的 args 可以按任何顺序应用,包括在未标记的 args 之前或之后。这将允许他们首先保留 t 进行类型推断,但仍使用标准的 |&gt; 运算符。 Base 使用此范例效果很好(例如,参见 List,其中 map 的函数标有 ~f)。
  • @kevinji 的确,这是一个很好的观点,实际上在这个过程中很早就提出了这个问题。不幸的是,鲍勃只是因为他个人不喜欢它而吹嘘它。
  • 反对-&gt; 的另一个论点是它似乎破坏了我拥有的refmt 的任何版本。当遇到-&gt; 时,它表示存在语法错误。
  • 我个人更喜欢|&gt; 而不是-&gt;,但显然re-script 已弃用|&gt; 管道。假设 re-script 将成为 bucklescript/reasonml 的未来,我想任何想要使用 bs/rescript 的人都需要习惯-&gt; pipe
  • 我怀疑它实际上会被删除,因为这会破坏 OCaml 兼容性以及与大量库的向后兼容性。但即使是这样,在用户空间中重新添加也是微不足道的。
【解决方案2】:

|&gt; 通常被称为“管道转发”。这是一个在更广泛的 OCaml 社区中使用的辅助函数,而不仅仅是 ReasonML。它将左侧的参数作为 last 参数“注入”到右侧的函数中:

0 |> f       == f(0)
0 |> g(1)    == g(1, 0)
0 |> h(1, 2) == h(1, 2, 0)
// and so on

-&gt; 被称为“管道优先”,它是一种新的语法糖,将左侧的参数注入到函数的 first 参数位置右边的数据构造函数:

0 -> f       == f(0)
0 -> g(1)    == g(0, 1)
0 -> h(1, 2) == h(0, 1, 2)
0 -> Some    == Some(0)

请注意,-&gt; 特定于 BuckleScript,即编译为 JavaScript 时。它在编译为本机时不可用,因此不可移植。更多细节在这里:https://reasonml.github.io/docs/en/pipe-first

【讨论】:

    猜你喜欢
    • 2010-10-02
    • 2011-12-12
    • 2010-09-16
    • 2012-03-14
    • 2012-02-06
    • 2011-02-25
    • 2011-11-22
    • 2015-03-26
    • 2013-08-19
    相关资源
    最近更新 更多