title | description | canonical |
---|---|---|
Pipe | The Pipe operator (->) | /docs/manual/latest/pipe |
ReScript provides a tiny but surprisingly useful operator ->
, called the "pipe", that allows you to "flip" your code inside-out. a(b)
becomes 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:
<CodeTab labels={["ReScript", "JS Output"]}>
validateAge(getAge(parseData(person)))
validateAge(getAge(parseData(person)));
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:
<CodeTab labels={["ReScript", "JS Output"]}>
person->parseData->getAge->validateAge
validateAge(getAge(parseData(person)));
Basically, parseData(person)
is transformed into person->parseData
, and getAge(person->parseData)
is transformed into person->parseData->getAge
, etc.
This works when the function takes more than one argument too.
<CodeTab labels={["ReScript", "JS Output"]}>
a(one, two, three)
a(one,two,three);
is the same as
<CodeTab labels={["ReScript", "JS Output"]}>
one->a(two, three)
a(one,two,three);
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.
JavaScript's APIs are often attached to objects, and often chainable, like so:
constresult=[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:
<CodeTab labels={["ReScript", "JS Output"]}>
@bs.sendexternalmap: (array<'a>, 'a=>'b) =>array<'b> ="map" @bs.sendexternalfilter: (array<'a>, 'a=>bool) =>array<'a> ="filter"typerequest @bs.valexternalasyncRequest: unit=>request="asyncRequest" @bs.sendexternalsetWaitDuration: (request, int) =>request="setWaitDuration" @bs.sendexternalsend: request=>unit="send"
// Empty output
You'd use them like this:
<CodeTab labels={["ReScript", "JS Output"]}>
letresult=Js.Array2.filter( Js.Array2.map([1, 2, 3], a=>a+1), a=>mod(a, 2) ==0 ) send(setWaitDuration(asyncRequest(), 4000))
varresult=[1,2,3].map(function(a){returna+1|0;}).filter(function(a){returna%2===0;});asyncRequest().setWaitDuration(4000).send();
This looks much worse than the JS counterpart! Clean it up visually with pipe:
<CodeTab labels={["ReScript", "JS Output"]}>
letresult= [1, 2, 3] ->map(a=>a+1) ->filter(a=>mod(a, 2) ==0) asyncRequest()->setWaitDuration(4000)->send
varresult=[1,2,3].map(function(a){returna+1|0;}).filter(function(a){returna%2===0;});asyncRequest().setWaitDuration(4000).send();
You can pipe into a variant's constructor as if it was a function:
<CodeTab labels={["ReScript", "JS Output"]}>
letresult=name->preprocess->Some
varresult=preprocess(name);
We turn this into:
<CodeTab labels={["ReScript", "JS Output"]}>
letresult=Some(preprocess(name))
varresult=preprocess(name);
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:
letaddTo7= (x) =>add3(3, x, 4) letaddTo7=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:
<CodeTab labels={["ReScript", "JS Output"]}>
makePerson(~age=47, ()) ->namePerson("Jane")
namePerson(makePerson(47),"Jane");
If you have a name that you want to apply to a person object, you can use a placeholder:
<CodeTab labels={["ReScript", "JS Output"]}>
getName(input) ->namePerson(personDetails, _)
var__x=getName(input);namePerson(personDetails,__x);
This allows you to pipe into any positional argument. It also works for named arguments:
<CodeTab labels={["ReScript", "JS Output"]}>
getName(input) ->namePerson(~person=personDetails, ~name=_)
var__x=getName(input);namePerson(personDetails,__x);
You might see usages of another pipe, |>
, in some codebases. These are deprecated.
Unlike ->
pipe, the |>
pipe puts the subject as the last (not first) argument of the function. a |> f(b)
turns into f(b, a)
.
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