What's the difference between -> and |> in reasonml?
Asked Answered
H

2

18

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

Huntley answered 2/4, 2019 at 12:16 Comment(1)
There are significant differences between the two, that are not obvious at first sight. Javier Chávarri gave a comprehensive comparison: javierchavarri.com/data-first-and-data-last-a-comparisonHemispheroid
A
36

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.

Aishaaisle answered 2/4, 2019 at 14:12 Comment(5)
A separate operator for BuckleScript could have been avoided had they just used labeled arguments, since labeled args can be applied in any order, including before or after unlabeled args. This would have allowed them to keep t first for type inference but still use the standard |> operator. Base uses this paradigm to great effect (e.g. see List, where the function to map is labeled with ~f).Enact
@kevinji Indeed, that's a great point and it's actually been raised both early and often during this process. Unfortunately Bob blows it off simply because he personally doesn't like it.Aishaaisle
Another argument against -> is that it seems to break whatever version of refmt I have. When it comes across a -> it says that there is a syntax error.Maricruzmaridel
I would personally prefer |> over -> but apparently re-script has deprecated the |> pipe. assuming re-script will be the future of bucklescript/reasonml I guess any one who wants to work with bs/rescript will need to get used -> pipeBrandie
I doubt it will actually be removed, as that would break OCaml compatibility as well as backwards-compatibility with a large number of libraries. But even if it is, it's trivial to add back in user-space.Aishaaisle
C
11

|> is usually called 'pipe-forward'. It's a helper function that's used in the wider OCaml community, not just ReasonML. It 'injects' the argument on the left as the last argument into the function on the right:

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

-> is called 'pipe-first', and it's a new syntax sugar that injects the argument on the left into the first argument position of the function or data constructor on the right:

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

Note that -> is specific to BuckleScript i.e. when compiling to JavaScript. It's not available when compiling to native and is thus not portable. More details here: https://reasonml.github.io/docs/en/pipe-first

Counterinsurgency answered 2/4, 2019 at 13:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.