->和有什么区别?和|>出于理性吗? [英] What's the difference between -> and |> in reasonml?

查看:107
本文介绍了->和有什么区别?和|>出于理性吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

解决方案

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)
 

聪明的功能设计

现在事实证明,大多数函数都在"main"自变量上操作,我们可以将其称为函数的对象". List函数通常在特定列表上运行,例如,一次不会运行多个(当然,当然也会发生).因此,将main参数放在最后可以使您更轻松地编写函数.例如,使用几个经过精心设计的函数,定义一个将可选值列表转换为具有默认值的实际值列表的函数非常简单:

 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)
 

但这仅在main参数为last的情况下有效,因为它依赖于通过currying的部分函数应用.

然后... BuckleScript

接着是Bob,他是第一作者BuckleScript的作者,目的是将OCaml代码编译为JavaScript.原因采用了BuckleScript,然后Bob继续为BuckleScript创建一个名为Belt的标准库. Belt几乎忽略了上面我通过将主参数首先解释的所有内容.为什么?这有待解释,但据我所知,主要是因为JavaScript开发人员更熟悉 1 .

Bob确实认识到管道运算符的重要性,因此他创建了自己的管道优先运算符|.,该运算符仅与BuckleScript 2 一起使用.然后,Reason开发人员认为这看起来有点丑陋且缺乏方向,因此他们提出了->运算符,该运算符转换为|.并完全像它一样工作...除了它的优先级不同,因此不会. 3

结论

管道优先运算符本身并不是一个坏主意.但是在BuckleScript中实现和执行它的方式引起了很多混乱.它具有出乎意料的行为,会鼓励不良的功能设计,除非一概而论 4 ,并且根据调用的函数类型在不同的管道运算符之间进行切换时会施加沉重的认知负担. /p>

因此,我建议避免使用管道优先运算符(->|.),而应将管道转发(|>)与list |> List.map(...) |> Belt.List.keep(_, ...).


1 在与类型推断的交互方式上也存在一些细微的差异,因为类型是从左到右推断的,但这对任何一种样式IMO都不明显.

2 因为它需要语法转换.与管道转发不同,它不能仅作为普通运算符实现.

3 例如,list |> List.map(...) -> Belt.List.keep(...) 无法正常工作

4 这意味着无法使用几乎所有在管道优先运算符存在之前创建的库,因为这些库当然是在考虑原始管道转发运算符的情况下创建的.这有效地将生态系统一分为二.

A period of intense googling provided me with some examples where people use both types of operators in one code, but generally they look just like two ways of doing one thing, they even have the same name

解决方案

tl;dr: The defining difference is that -> pipes to the first argument while |> pipes to the last. That is:

x -> f(y, z) <=> f(x, y, z)
x |> f(y, z) <=> f(y, z, x)

Unfortunately there are some subtleties and implications that makes this a bit more complicated and confusing in practice. Please bear with me as I try to explain the history behind it.

Before the age of pipe

Before there were any pipe operators, most functional programmers designed most functions with the "object" that the the function operates as the last argument. This is because function composition is made much easier with partial function application, and partial function application is made much easier in curried languages if the arguments not applied are at the end.

Currying

In a curried language, every function takes exactly one argument. A function that appears to take two arguments is really a function that takes one argument, but then returns another function that takes another argument and in turn returns the actual result. Therefore these are equivalent:

let add = (x, y) => x + y
let add = x => y => x + y

Or rather, the first form is just syntax sugar for the second form.

Partial function application

This also means we can easily partially apply a function by just providing the first argument, which will have it return a function that accepts the second argument before producing a result:

let add3 = add(3)
let result = add3(4) /* result == 7 */

Without currying, we'd have to instead wrap it in a function, which is much more cumbersome:

let add3 = y => add(3, y)

Clever function design

Now it turns out that most functions operate on a "main" argument, which we might call the "object" of a function. List functions usually operate on a specific list, for example, not several at once (although that does occur too, of course). And therefore, putting the main argument last enables you to compose functions much more easily. For example, with a couple of well-designed functions, defining a function to transform a list of optional values into a list of actual values with defaults is as simple as:

let values = default => List.map(Option.defaultValue(default)))

While functions designed with the "object" first would require you to write:

let values = (list, default) =>
  List.map(list, value => Option.defaultValue(value, default)))

The dawn of the pipe era (which, ironically, wasn't pipe-first)

From what I understand, someone playing around in F# discovered a commonly occurring pipeline pattern and thought it was cumbersome to either come up with named bindings for intermediate values or nest the function calls in backwards order using too many damn parentheses. So he invented the pipe-forward operator, |>. With this, a pipeline could be written as

let result = list |> List.map(...) |> List.filter(...)

instead of

let result = List.filter(..., List.map(..., list))

or

let mappedList = List.map(..., list)
let result = List.filter(..., mapped)

But this only works if the main argument is last, because it relies on partial function application through currying.

And then... 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

Conclusion

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.

I would therefore recommend avoiding the pipe-first operator (-> or |.) and instead use pipe-forward (|>) with a placeholder argument (also exclusive to Reason) if you need to pipe to an "object"-first function, e.g. 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

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.

这篇关于-&gt;和有什么区别?和|&gt;出于理性吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆