Pipe first vs. currying

One more beginner question from me for now! :sweat_smile:

So I’ve only ever used pipe-last languages, and for me piping is absolutely tied to currying. It makes perfect sense that the pipe operator has the signature (data: 'a, f: 'a => 'b) => 'b, and that when you partially apply a function, it can be used on the right side of that operator.

I can see the advantages of pipe-first and I’m interested to try it in anger, but I need to get my head around how it fits with automatic currying. I was fully expecting currying to work in reverse as well. That is, I expected eg. data->Array.map(f) to be equivalent to Array.map(f)(data), but it’s not. The thing that I find hard to reconcile is that the expression f(x) (for a binary function f) has a different meaning depending on whether or not it’s in a pipeline.

I wonder how useful automatic currying is, given the above issue, and the fact that most functions will be written data-first? Is it used in practice? I imagine that in most cases it would be far preferable to be explicit by using placeholder arguments. In the presence of pipe-first and placeholder arguments, is automatic currying even a useful language feature?

Would it be at all possible for the language to implement currying in reverse order, such that Array.map(f) outside of a pipeline results in a function array<'a> => array<'b>?

1 Like

I think it would be useful for the docs to be more explicit about some of this. There should at least be a section on currying with an example of using a partially applied function, clarifying that arguments are filled in first to last order.

A warning about the context sensitive meaning of f(x) would also be valuable, eg. be careful that if you extract f(x) out of a pipeline a->f(x) into a let binding, you’ll need to change it to let g = f(_, x).

2 Likes

Automatic currying is convenient, but can bloat the generated code and lead to accidents. I still like it but I’ve come to accept that in ReScript it’s discouraged.

I agree with the ReScript philosophy of using pipe first because a lot of JS/TS environments are, effectively, data first.

For example this JS:
obj.add(2).subtract(4).multiply(3)

can (with some fancy bindings) be expressed in ReScript as:
obj->add(2)->subtract(4)->multiply(3)

So I tend towards pipe first because it lets me write code more in a JS structure rather than traditional FP. This is beneficial when teaching TS programmers how to use ReScript.

5 Likes

I like your posts, they really hit the point.
Actually there’s a big discussion about making functions uncurried by default: Uncurried by default? - #19 by bsansouci

Like I said I appreciate the benefits of pipe first and I look forward to trying it for real. My question was more about how you conceptually reconcile the two features, or how you navigate the ambiguity of partial application inside vs. outside pipelines; and also to understand what the status of currying is in real-world use. Seems like overall it is downplayed (that explains its near-absence from the docs) and underused. I’m following the other discussion too.

I’m a ReScript production user, but still can hardly remember a place where we intensionally make use of currying.