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.
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 ->
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 |>
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 ->
pipe –
Brandie |>
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
© 2022 - 2024 — McMap. All rights reserved.