I'd like to propose three improvements to Roc’s pattern matching syntax for better conciseness and readability.
First an example of current pattern matching syntax:
Language en sp : [English en, Spanish sp]
onEnglish : Language en1 sp, (en1 -> Language en sp) -> Language en sp
onEnglish = \lang, f ->
when lang is
English en -> f en
Spanish sp -> Spanish sp
mapEnglish : Language en1 sp, (en1 -> en) -> Language en sp
mapEnglish = \lang, f ->
when lang is
English en -> English (f en)
Spanish sp -> Spanish sp
1) Single-line pattern matching
This idea was previously discussed on #ideas > single line pattern matching, but I think it should be revisited.
Proposed syntax:
onEnglish = \lang, f -> when lang is (English en -> f en) (Spanish sp -> Spanish sp)
mapEnglish = \lang, f -> when lang is (English en -> English (f en)) (Spanish sp -> Spanish sp)
2) Use when without is and make it pipeable
Eliminating is and making when pipeable would streamline code.
Proposed syntax:
onEnglish = \lang, f ->
lang |> when
English en -> f en
Spanish sp -> Spanish sp
mapEnglish = \lang, f ->
lang |> when
English en -> English (f en)
Spanish sp -> Spanish sp
3) Implicit exhaustive pattern matching
In many cases, default patterns like Spanish sp -> Spanish sp are redundant and repetitive. By making these cases implicit, I think we could simplify pattern matching.
Proposed syntax:
onEnglish = \lang, f ->
when lang is
English en -> f en
mapEnglish = \lang, f ->
when lang is
English en -> English (f en)
And finally an example of combined proposed syntax (single-line + pipeable + implicit)
onEnglish = \lang, f -> lang |> when (English en -> f en)
mapEnglish = \lang, f -> lang |> when (English en -> English (f en))
Nice! I'm sold for 1 and 2, but hesitating about 3. Would this default behavior kick in only when the existing branches are of the same type as the input to when? I often find myself in the situation where some branches just return the input, but I think I'd prefer the syntax to be explicit rather than implicit. Adding a final x -> x branch isn't that long.
Regarding 2, I'm wondering whether when is might be more consistent. I usually don't like sprinkling useless keywords, but as a beginner I think I would expect when x is to be pipeable as x |> when is. Really not a strong opinion, more like an open question.
Yeah I don't have a strong opinion about the keyword is I just didn't know what to do with it in a pipeable version.
Regarding idea 3, it came up while I was looking at Result.roc
I rewrote it like this (I omitted the comments):
module [Result, isOk, isErr, withDefault, onOk, onErr, mapOk, mapErr, mapBoth, map2]
Result ok err : [Ok ok, Err err]
match : Result ok err, (ok -> new), (err -> new) -> new
match = \result, fa, fb ->
when result is
Ok a -> fa a
Err b -> fb b
isOk : Result ok err -> Bool
isOk = \result -> result |> match (\_ -> Bool.true) (\_ -> Bool.false)
isErr : Result ok err -> Bool
isErr = \result -> result |> match (\_ -> Bool.false) (\_ -> Bool.true)
withDefault : Result ok err, ok -> ok
withDefault = \result, default -> result |> match (\ok -> ok) (\_ -> default)
# renamed from `try` to `onOk` for consistency
onOk : Result ok1 err, (ok1 -> Result ok2 err) -> Result ok2 err
onOk = \result, transform -> result |> match transform (\err -> Err err)
onErr : Result ok err1, (err1 -> Result ok err2) -> Result ok err2
onErr = \result, transform -> result |> match (\ok -> Ok ok) transform
# renamed from `map` to `mapOk` for consistency
mapOk : Result ok1 err, (ok1 -> ok2) -> Result ok2 err
mapOk = \result, transform -> result |> match (\ok -> Ok (transform ok)) (\err -> Err err)
mapErr : Result ok err1, (err1 -> err2) -> Result ok err2
mapErr = \result, transform -> result |> match (\ok -> Ok ok) (\err -> Err (transform err))
mapBoth : Result ok1 err1, (ok1 -> ok2), (err1 -> err2) -> Result ok2 err2
mapBoth = \result, okTransform, errTransform ->
result |> match (\ok -> Ok (okTransform ok)) (\err -> Err (errTransform err))
map2 : Result ok1 err, Result ok2 err, (ok1, ok2 -> ok3) -> Result ok3 err
map2 = \firstResult, secondResult, transform ->
when (firstResult, secondResult) is
(Ok first, Ok second) -> Ok (transform first second)
(Err err, _) -> Err err
(_, Err err) -> Err err
I am not a fan of 1. It is very dense and quite hard to read.
Some form of 2 sounds quite nice.
I think 3 is a bad idea. We should require an explicit match. All it takes is x -> x or _ -> lang.
There was a discussion about 2 at some point. Ocaml (and other ML's?) have function which does this. Haskell has \case if LambdaCase is enabled. Unison has something similar as well, but I forgot the keyword.
I remember wanting it back then in roc, but by now, I think it shouldn't have it to keep in line with roc's explicitness
I don’t think should do 1. It seems like there is little benefit to being able to avoid a few extra lines and it seems hard to read.
I can see the appeal of 2 for being able to seamlessly add a when is into a longer pipeline, but I will say that this hasn’t been much of an issue for me so I don’t feel very strongly about it.
I think 3 will cause problems. I really like knowing that if I write out all patterns explicitly, the compiler will tell me if I missed something or if a new tag gets added. With 3, I would lose the ability to do that in some circumstances because a new tag being added would just switch to the compiler defaulting that branch. It could also lead to weirdness when type annotations are omitted where an unexpected tag gets included in the union resulting from a pattern match because a branch was silently defaulted.
2 is something you can do in elixir, the syntax is
foo
|> case do
Instead of
case foo do
I think it's nice, but definitely not essential. Beginners would usually be surprised that piping into a case is valid, but they'd get over it quick.
Sorry to be a party pooper, but I don't like the results of this proposal.
We inherit a lot from Elm, and one of the great decisions I think it made that we currently borrow is the readability that comes from forcing control flow to be made obvious by:
Even though we make an exception for single-line if-else expressions (which I think should be used sparingly), I don't think that the single line when statements are very readable. if-else is tolerable because it's a single condition, but when statements with multiple statements get jumbled if they're on one line. If there's only one branch for the when statement, that can be rewritten as a destructure. So I think that the first proposal is more concise, but at the sacrifice of readability.
If we support piping for when, then that incentivizes against the first Elm benefit of control flow being in separate statements. I think that any complex when expression is almost always better served by being in a separate statement or extracted to a helper function. So though it helps put more behavior into pipelines, I think defining intermediate state as variables is more readable than putting the entire function body into a pipeline, so we should incentivize Roc devs to do that.
The last proposal falls into a different bucket, since the issue is more that it breaks our current type inference. If we allow users to avoid writing the trivial cases entirely, then the following code can no longer be inferred to have a closed tag union with only two variants unless you put a type annotation, which defeats the point:
color =
when mood is
Happy -> Yellow
# mood would need to be inferred as `[Happy]*`, which is less safe than `[Happy, Sad]`
I think I could maybe be convinced that adding some form of the pipe-able when would be okay, but I think the cases where it makes code less readable seem at a glance to be much more numerous than the cases where readability is improved.
The place where I use \case (equivalent of piped when) in haskell is not in piping. I use it to not have to name the intermediate value. Let's say I want to write an eval function for a hypothetical Expr : [Int I64, Add Expr Expr]. Without piped when it would need to be:
eval = \expr ->
when expr is
Int n -> n
Add l r -> eval l + eval r
With piped when, you don't need the intermediate expr:
eval = when
Int n -> n
Add l r -> eval l + eval r
In haskell I use this a lot (PL theory background at Uni), because it is more concise and more point-free. In Roc I don't want it anymore for 2 reasons. Firstly, in roc extra arguments would generally be in front of expr to enable piping. This would make the piped when impossible to use this way. The second reason is that I feel like it goes against Roc's goal of having functions be explicit and it encourages point-free style which roc explicitly doesn't want
Last updated: Jun 16 2026 at 16:19 UTC