ReScript provides a tiny but surprisingly useful operator
->, called the "pipe", that allows you to "flip" your code inside-out.
b->a. It's a simple piece of syntax that doesn't have any runtime cost.
Why would you use it? Imagine you have the following:
This is slightly hard to read, since you need to read the code from the innermost part, to the outer parts. Use pipe to streamline it:
parseData(person) is transformed into
getAge(person->parseData) is transformed into
This works when the function takes more than one argument too.
is the same as
This also works with labeled arguments.
Pipes are used to emulate object-oriented programming, e.g. what's
myStudent.getName in other languages like Java would be
myStudent->getName in ReScript (aka
getName(myStudent)). This allows us to have the readability of the good parts of OOP without its downside of dragging in a huge class system just to call a function on a piece of data.
Do not to abuse pipes; they're a means to an end. Inexperienced engineers sometimes shape a library's API to take advantage of the pipe. This is backward.
This section requires understanding of our binding API.
const result = [1, 2, 3].map(a => a + 1).filter(a => a % 2 === 0); asyncRequest() .setWaitDuration(4000) .send();
Assuming we don't need the chaining behavior above, we'd bind to each case this using
bs.send from the aforementioned binding API page:
@send external map: (array<'a>, 'a => 'b) => array<'b> = "map" @send external filter: (array<'a>, 'a => bool) => array<'a> = "filter" type request @val external asyncRequest: unit => request = "asyncRequest" @send external setWaitDuration: (request, int) => request = "setWaitDuration" @send external send: request => unit = "send"
You'd use them like this:
This looks much worse than the JS counterpart! Clean it up visually with pipe:
You can pipe into a variant's constructor as if it was a function:
We turn this into:
Note that using a variant constructor as a function wouldn't work anywhere else beside here.
A placeholder is written as an underscore and it tells ReScript that you want to fill in an argument of a function later. These two have equivalent meaning:
let addTo7 = (x) => add3(3, x, 4) let addTo7 = add3(3, _, 4)
Sometimes you don't want to pipe the value you have into the first position. In these cases you can mark a placeholder value to show which argument you would like to pipe into.
Let's say you have a function
namePerson, which takes a
person then a
name argument. If you are transforming a person then pipe will work as-is:
If you have a name that you want to apply to a person object, you can use a placeholder:
This allows you to pipe into any positional argument. It also works for named arguments:
You might see usages of another pipe,
|>, in some codebases. These are deprecated.
-> pipe, the
|> pipe puts the subject as the last (not first) argument of the function.
a |> f(b) turns into
For a more thorough discussion on the rationale and differences between the two operators, please refer to the Data-first and Data-last comparison by Javier Chávarri