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 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.
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.
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)
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)))
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.
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.
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.
|>
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With