I've been reading the FAQ about non default currying (https://github.com/roc-lang/roc/blob/main/FAQ.md#currying-and-the--operator) where is mentioned that
Of note, one possible design is to have currying while also having
|>
pass the last argument instead of the first. This is what Elm does, and it makes pipeline-friendliness and curry-friendliness the same thing. However, it also means that either|> Str.concat "!"
would add the"!"
to the front of the string, or elseStr.concat's
arguments would have to be flipped - meaning thatStr.concat "Hello, World" "!"
would evaluate to"!Hello, World"
.
And while I agree that Str.concat "Hello, World" "!"
should evaluate to "Hello, World!"
, i'm not entirely sure as to why|>
should not pass the last parameter. That would enable functions like map to keep the map func vals
shape (which makes more sense in my opinion). Are there any downsides?
Conversely,
One reason the |> operator injects the value as the first argument is to make it work better with functions where argument order matters. For example, these two uses of List.append are equivalent:
List.append ["a", "b", "c"] "d"
["a", "b", "c"]
|> List.append "d"
makes sense, but seems like an arbitrary choice when compared with
List.prepend "a" [ "b", "c", "d"]
where the following:
["b", "c", "d"]
|> List.prepend "a"
wouldn't work as the list would be passed as the first parameter.
This is the same pattern used on Elixir (and F#?) - so maybe it is the default expectation for non-curried FP langs?
I think the way Gleam does it is the best for this as it removes confusion while it becomes more powerful: https://gleam.run/book/tour/functions.html#function-capturing
The Gleam example indeed seems nice (although it would add more syntax). And would work with either prepend/append by allowing functions to define the order of arguments in a way that makes sense for the function, while not having to list argument in such a way they work nicely with piping. Another option would be dropping piping as a whole and using multiline bodies instead. (which will give the opportunity for naming variables to improve readability). Come to think of it, what are benefits from piping over assigning it to variables?
FWIW, I think List.prepend
's parameters should switch places to support piping
However, if we can make it so that in 80% of situations you want to pipe the first parameter (which is how I perceive it today), I think the remaining 20% are adequately supported by
["b", "c", "d"]
|> x -> List.prepend "a" x
7 extra characters to accommodate the nonstandard case :shrug:
FWIW, I think List.prepend's parameters should switch places to support piping
That would make sense in a "pipe centric" design, but the (mentioned in tutorial) reason for piping using the first argument in the first place is exactly avoiding that scenario.
However, if we can make it so that in 80% of situations you want to pipe the first parameter (which is how I perceive it today), I think the remaining 20% are adequately supported by
How often is piping expected to be used? Ordering parameters would make sense if the bulk of the code would use piping. But if not, those same 7 extra characters would support piping (in the case of piping being the exception). Keeping the function types perhaps more sane.
I personally try to design all of my Roc code around piping, but maybe I'm going overboard! :sweat_smile:
Me as well :) (not only Roc but pretty much any functional language)
I just found this related stream as well, for the reader: https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/Argument.20Ordering.20in.20Pipelines
My experience in functional languages is limited to Haskell, where piping is not really a thing (or i'm missing out on something). The closest alternative is function composition (.
) but this is more like (<|
) in the sense that it reads from right to left. But AFAIK this won't be a feature in roc. That is perhaps the reason I feel weird about ordering arguments just to suit piping.
An example of my love for piping: https://github.com/JanCVanB/advent-of-code-2021/blob/main/solutions/day-1-sonar-sweep/solution_1_part_1.roc
I think the reason it targets the first parameter is that most function first pass in the main thing they are operating on and then pass in all of the parameters that define how to operate on that thing.
Of course you could swap it, but i think going from most important to more extraneous is pretty natural
Oh, also if piping was the last parameter then it would clash with the convention of making callback function(s) the last parameter(s). I'm not sure how important that is, but it seems tricky
When using pipes, you can't use backpassing, so maybe that doesn't technically matter, but I guess that would limit you to either backpassing or pipes, not both.
imo piping the last argument would only make sense if there was automatic currying... (having automatic currying would be pretty awesome for other reasons like applicatives but that is besides the point :sweat_smile:)
I've gotten feedback a few times that this section of the FAQ wasn't coming across well, so I took a pass at revising it...feedback welcome! https://github.com/roc-lang/roc/blob/5c8f91be887d6641d116d40133329b84a607e87d/FAQ.md#currying-and-the--operator
(the other sections are unchanged; I only edited the linked section, Currying and the |>
operator)
Regarding this bit: https://github.com/roc-lang/roc/blob/5c8f91be887d6641d116d40133329b84a607e87d/FAQ.md?plain=1#L362-L384
Wouldn't that work the other way around as well? if the numbers parameter would be a larger calculation than the function?
Good stuff Richard, I find this explanation more convincing than the previous one. :+1:
@J.Teeuwissen that's true in theory but in practice I find it's far more common for the function to be the larger part.
that's true in theory but in practice I find it's far more common for the function to be the larger part.
Isn't the data part being the larger/(more processed) part the reason for pipelines to exist in the first place? In either case it will depend on the specific use case and both can be relieved with intermediate variables. But I guess you have to choose either one anyway.
When you compared the 'size' of function vs data I figured it meant the size of the code text. Not sure what other size metric would apply to both.
Yes, that's what meant. With larger/(more processed) I meant that more processing requires more code. But in the case of piping this is not done inline of course.
really like the new FAQ section as well :ok:
Yeah the resulting data may in fact be more complex than any individual transformation function step, but that complexity is (or can be) spread across many piped steps. For example, if you wanted to declare some nested data monster, you could instead declare it as a simpler data structure piped through some nesting transformations.
JanCVanB said:
The same can be said of functions, right? Especially in a functional language.
I'm new to FP (Roc is my first PureFPL), but it seems to me that functions deconstructed into simplest lambda compinents will usually be more verbose per piece than data deconstructed into simplest numbers & arguments
but at the end of the day maybe it's the same
That's what first class citizenship will get you :rolling_on_the_floor_laughing:
J.Teeuwissen said:
My experience in functional languages is limited to Haskell, where piping is not really a thing (or i'm missing out on something). The closest alternative is function composition (
.
) but this is more like (<|
) in the sense that it reads from right to left. But AFAIK this won't be a feature in roc. That is perhaps the reason I feel weird about ordering arguments just to suit piping.
In Haskell there is $
which behaves like <|
and the somewhat newer &
(in Data.Function
) that behaves like |>
.
(In these cases of course on the rightmost argument, or rather, on the 'single' argument that exists in a curried language.)
Of course, in Haskell it is also common to combine functions using .
or <<<
(right-to-left) or >>>
(left-to-right; these last two are fromControl.Category
/Control.Arrow
) 'point-free' composition.
Ordering the most significant parameter first to suit piping (in Roc, F#, Elixir, etc.) is similar to ordering the most significant argument last to support a point-free style of programming (in Haskell, PureScript, etc.).
Last updated: Jul 06 2025 at 12:14 UTC