Stream: ideas

Topic: Mapping pipe operator


view this post on Zulip LoipesMas (Dec 05 2023 at 18:26):

Maybe I'm one of the very few that like the operators in Haskell. Maybe it's because I haven't had to write or read any production Haskell. But I kinda miss more expressive operators. For example, in Roc, I find myself doing:

getList input |> List.map func1 |> List.map func2

or

tryThis input |> Result.map func1 |> Result.map func2

and this can get pretty verbose. I wish we had a "map-pipe" operator, for example &>, which would apply the function to values inside the variable. So it would look like this, instead:

getList input &> func1 &> func2

Now, it would be best if it would work for all functors, but introducing this term might be confusing and scary. But I like operating on more abstract levels, i.e., "just do this to stuff inside". And having Result.map all over the code makes it less enjoyable to work with Results.

If we had pointfree composition, this could be solved instead by doing:

getList input |> Result.map (func2 . func1)

but we don't, for other reasons. And maybe we shouldn't have &> for the same reasons, but we already have |>, so why not one more?
What are your opinions?

view this post on Zulip Luke Boswell (Dec 05 2023 at 18:31):

I personally prefer the more verbose version because it is more explicit and really easy to follow. I can see immediately that this is a Result and not some other thing. Is the primary concern writing or reading this? Does it help to split onto separate lines? I guess being verbose the issue is that it spreads everything out which makes it harder to read all in one screen at once?

view this post on Zulip Brian Carroll (Dec 05 2023 at 18:37):

The general design philosophy is to keep the language as simple and small as possible. So where Haskell or C++ ask "why not one more?", Roc tends to ask "do we really need it?" :joy:

view this post on Zulip LoipesMas (Dec 05 2023 at 18:43):

Luke Boswell said:

I personally prefer the more verbose version because it is more explicit and really easy to follow. I can see immediately that this is a Result and not some other thing. Is the primary concern writing or reading this? Does it help to split onto separate lines? I guess being verbose the issue is that it spreads everything out which makes it harder to read all in one screen at once?

Being explicit is a fair point, but I don't think that we should rely on the map function to convey type information. Mapping is just mapping and what the outer type is should be inferred from context (e.g., the pipeline ends in Result.withDefault).
Separate lines kinda help, but then I feel like we waste a lot of screen space. I've also noticed that my expressions/pipelines usually just about fit on one line, although less readable line, balancing in the grey area between "short enough for one line" and "long enough for separate lines".
Writing is also a bit more painful, but that might be eased with a proper language server.

view this post on Zulip LoipesMas (Dec 05 2023 at 18:44):

Brian Carroll said:

The general design philosophy is to keep the language as simple and small as possible. So where Haskell or C++ ask "why not one more?", Roc tends to ask "do we really need it?" :joy:

I know that Roc is pretty strict about such ideas, that's why I'm not afraid of proposing this and it being implemented without good reasons ;]

view this post on Zulip Brendan Hansknecht (Dec 05 2023 at 18:45):

personally, my issue with the function shown above is the potential perf cost.

which do you want? They can have wildly different performance charcateristics in some cases:

getList input |> List.map func1 |> List.map func2
getList input |> List.map \x -> x |> func1 |> func2

view this post on Zulip Brendan Hansknecht (Dec 05 2023 at 18:48):

In haskell (laziness) and rust (iterators) you tend to get the second one. In roc, you have to explicitly pick which one you want (at least currently)

view this post on Zulip LoipesMas (Dec 05 2023 at 18:53):

Not having laziness or iterators is also something I miss :sweat_smile:
I think second one is preferable in most cases, but writing that in Roc is even more verbose/uncomfortable (because of the lack of currying).
And I know those are not critical issues, but it's a part of developer experience and developer experience is important

view this post on Zulip Luke Boswell (Dec 05 2023 at 18:57):

Is there something I can read which explains the general reasoning behind this distinction? Is it not possoble for the compiler to see they are doing the same thing?

view this post on Zulip Brendan Hansknecht (Dec 05 2023 at 19:02):

We could add a compiler optimization for this in the future

view this post on Zulip Brendan Hansknecht (Dec 05 2023 at 19:03):

That said, you don't always want the second version. really depends on a number of things. But I do think it is the better default

view this post on Zulip LoipesMas (Dec 05 2023 at 19:04):

In what scenario would you prefer first version instead of the second one?

view this post on Zulip Anton (Dec 05 2023 at 19:07):

We definitely care about the developer experience. We don't have currying because of the negative consequences to the beginner developer experience.

view this post on Zulip LoipesMas (Dec 05 2023 at 19:14):

I didn't mean to imply that you don't!
Not having currying also have negative consequences. Everything has its upsides and downsides, so deciding where to draw the line is definitely tricky. And you can't appeal to everyone

view this post on Zulip Anton (Dec 05 2023 at 19:21):

I think in general we prefer to keep things simple even when it makes things somewhat more verbose.

view this post on Zulip Brendan Hansknecht (Dec 05 2023 at 19:50):

In what scenario would you prefer first version instead of the second one?

Performance wise, it depends on the internal calculations and the output type sizes. If the input and output types are the same size, we can do List.map in place. On top of that, many small hotter loops can often be faster than if you make the loop body too big. So I have seen similar things in hot loops parsing bytes for example (though that would I guess be two List.walk in a row rather than two List.map in a row, but it is the same concept fundamentally.).

view this post on Zulip LoipesMas (Dec 05 2023 at 19:51):

Anton said:

I think in general we prefer to keep things simple even when it makes things somewhat more verbose.

Fair. I may not agree on some decision, but I definitely still believe in The Vision™!


Last updated: Jun 16 2026 at 16:19 UTC