as mentioned on the "elif syntax" thread and previously on a forgotten thread :sweat_smile: - I'd like to propose a different if syntax that would use less reserved keywords and be less verbose for large if else if else if else... cases.
if
b <= 0 ->
"c'mon..."
b == 1 ->
"just one?"
b > 5 ->
"whoa there"
_ ->
"fine I guess"
we wouldn't need then and else as reserved keywords. the compiler could catch a lack of true or _ and warn the user. it feels a lot like when ... is so it would be familiar to Roc developers and there is basically no readability cost for increasing the number of cases (in comparison to if/else if)
this is inspired by the cond feature of the elixir language. I find it a bit weird there because it is just another way of achieving the same you could do with if/else if.
to clarify: today this example would look like this, right?
if b <= 0 then
"c'mon..."
else if b == 1 then
"just one?"
else if b > 5 then
"whoa there"
else
"fine I guess"
yeah
with extra vertical space but yeah
One of the cool things about this suggestion is that we could also change when .. is to be if .. is and further simplify the language, as in:
if
b > a ->
Stdout.line "b is greater than a"
a == b ->
Stdout.line "a and b are equal"
_ ->
Stdout.line "a is greater than b"
and
if word is
"YES" ->
Stdout.line "word is: YES"
"NO ->
Stdout.line "word is: NO"
_ ->
Stdout.line "word is not YES or NO"
another advantage is that ordering does not rely on a change of if to else if - so changes there would probably be more accurate in regards to version control diffs.
One downside of this is that you couldn’t have a single line if, but that might actually be good for readability
I like how the conditions align
single line if: if a > b -> "TRUE" _-> "False" could work somehow?
I don't think that would work in the general case - e.g. if foo Bar Baz -> "TRUE" - is Bar an argument to foo or part of the Bar Baz -> pattern?
maybe it could require a comma or sometthing
if a > b -> "TRUE", _-> "False"
that seems worse than else though
We could live without one line if statements?
I would gladly give up one line if statements for the glory of clean branches in general.
I think so, I've never been a fan of one line if
I tried this in a couple of places and it's cool when you have at least one else if, but it feels worse when you just have if/else
smallest : Num a, Num a -> Num a
smallest = \a, b ->
if a < b then
a
else
b
smallest : Num a, Num a -> Num a
smallest = \a, b ->
if
a < b ->
a
_ ->
b
I find the if/then/else version much more readable
More verbose could be:
ifTrue
b > a ->
Stdout.line "b is greater than a"
a == b ->
Stdout.line "a and b are equal"
_ ->
Stdout.line "a is greater than b"
and
if word is
"YES" ->
Stdout.line "word is: YES"
"NO ->
Stdout.line "word is: NO"
_ ->
Stdout.line "word is not YES or NO"
That underlines the difference between the two.
That would perhaps allow for better editor completion.
ifTrue feels like it really sticks out compared to the rest of the language. I think your previous proposal works well, I don't anticipate a problem for editor autocomplete because for one variant you'll be using a space and for the other a newline.
Another downside you can see in my example is that this introduces another level of indentation
I find the
if/then/elseversion much more readable
The short variable names in that example may create a unique worst case look
Anton said:
ifTruefeels like it really sticks out compared to the rest of the language. I think your previous proposal works well, I don't anticipate a problem for editor autocomplete because for one variant you'll be using a space and for the other a newline.
Agreed re the wierdness of it. (Good if conflating the two is not a problem).
Anton said:
I find the
if/then/elseversion much more readableThe short variable names in that example may create a unique worst case look
Ok, here's another example from roc-pg:
name =
if Set.contains reusedIndexes cmdIndex then
Batch.reuseName cmdIndex
else
""
name =
if
Set.contains reusedIndexes cmdIndex ->
Batch.reuseName cmdIndex
_ ->
""
It's close but I prefer the new style there
Oh, even without an else if?
yes
Ok, yeah I guess it's pretty subjective
At the expense of super easy refactoring...If is such a short word that it doesn't indent much. It could be written inline with the first condition.
name =
if Set.contains reusedIndexes cmdIndex ->
Batch.reuseName cmdIndex
_ ->
""
And really that would be pretty easy to refactor (without special editor assistance).
That indentation would make if .. is less distinctive (if when ..is were replaced by if..is. So with that indentaion then would we need to leave when ..is as is?
text =
if word is
"YES" ->
Stdout.line "word is: YES"
"NO ->
Stdout.line "word is: NO"
_ ->
Stdout.line "word is not YES or NO"
Not sure if the two would get too confusing then.
name =
if Set.contains reusedIndexes cmdIndex ->
Batch.reuseName cmdIndex
_ ->
""
With the _ -> not lining up with anything else this type of indentation would be unique in the language which I would try to avoid...
it would have to line up with the word after if.
Personally I'm ok with all the space around if, I was just trying it out with if inline to see how it looks.
inlining the first if would also create the "problem" that reordering the list of cases would require changing which has the if or not (bad for diffs)
@Agus Zubiaga
Agus Zubiaga said:
smallest : Num a, Num a -> Num a smallest = \a, b -> if a < b then a else b smallest : Num a, Num a -> Num a smallest = \a, b -> if a < b -> a _ -> bI find the
if/then/elseversion much more readable
It strikes me the same at first but maybe it's just discomfort with all the zen ( noise free code). :grinning_face_with_smiling_eyes:
I think it's quickly growing on me.
The proposed if syntax is basically the same as the when .. is syntax.
It fundamentally makes the language more consistent.
That makes it kind of difficult to argue with. If the conventional if..else syntax is preferable then we should re-evaluate the pattern match syntax.
if we desire to pursue the proposal, there's the question in my mind though how much syntactic differentiation we would want between when..is and if.
I think the extra indenting will be really annoying. That is actually my biggest annoyance with when as well. In complex functions it pushes everything to the right and makes it painful to read. Means that you need to start figuring out how to pull out subfunctions much much sooner if you want readability.
Brendan Hansknecht said:
I think the extra indenting will be really annoying. That is actually my biggest annoyance with
whenas well. In complex functions it pushes everything to the right and makes it painful to read. Means that you need to start figuring out how to pull out subfunctions much much sooner if you want readability.
It's one extra line of indentation right? And an extra line of (vertical) space:
if
b <= 0 ->
"c'mon..."
b == 1 ->
"just one?"
b > 5 ->
"whoa there"
_ ->
"fine I guess"
vs (the current syntax)
if b <= 0 then
"c'mon..."
else if b == 1 then
"just one?"
else if b > 5 then
"whoa there"
else
"fine I guess"
As a concrete example. Code from hashing for the roc Dict:
addBytes : LowLevelHasher, List U8 -> LowLevelHasher
addBytes = \@LowLevelHasher { originalSeed, state }, list ->
length = List.len list
seed = Num.bitwiseXor originalSeed wyp0
abs =
if length <= 16 then
if length >= 4 then
x = Num.shiftRightZfBy length 3 |> Num.shiftLeftBy 2
a = Num.bitwiseOr (wyr4 list 0 |> Num.shiftLeftBy 32) (wyr4 list x)
b =
(wyr4 list (Num.subWrap length 4) |> Num.shiftLeftBy 32)
|> Num.bitwiseOr (wyr4 list (Num.subWrap length 4 |> Num.subWrap x))
{ a, b, seed }
else if length > 0 then
{ a: wyr3 list 0 length, b: 0, seed }
else
{ a: 0, b: 0, seed }
else if length <= 48 then
hashBytesHelper16 seed list 0 length
else
hashBytesHelper48 seed seed seed list 0 length
combineState (@LowLevelHasher { originalSeed, state }) { a: abs.a, b: abs.b, seed: abs.seed, length: Num.toU64 length }
with proposed syntax:
addBytes : LowLevelHasher, List U8 -> LowLevelHasher
addBytes = \@LowLevelHasher { originalSeed, state }, list ->
length = List.len list
seed = Num.bitwiseXor originalSeed wyp0
abs =
if
length <= 16 ->
if
length >= 4 ->
x = Num.shiftRightZfBy length 3 |> Num.shiftLeftBy 2
a = Num.bitwiseOr (wyr4 list 0 |> Num.shiftLeftBy 32) (wyr4 list x)
b =
(wyr4 list (Num.subWrap length 4) |> Num.shiftLeftBy 32)
|> Num.bitwiseOr (wyr4 list (Num.subWrap length 4 |> Num.subWrap x))
{ a, b, seed }
length > 0 ->
{ a: wyr3 list 0 length, b: 0, seed }
_ ->
{ a: 0, b: 0, seed }
length <= 48 ->
hashBytesHelper16 seed list 0 length
_ ->
hashBytesHelper48 seed seed seed list 0 length
combineState (@LowLevelHasher { originalSeed, state }) { a: abs.a, b: abs.b, seed: abs.seed, length: Num.toU64 length }
Personally, I think that already reads worse due to indenting so many lines and some conditions being far enough apart that alignment doesn't necessarily help much. This function is not even that complex. This will be a lot worse in a function that already has a few level of nesting and potentially an inline if that needs to be rewriting to multiline format.
@Matthias Toepp your example is mixing indent sizes. If you are gonna use a two space indent with the new syntax, you should use a two space indent with the old syntax.
As an extra note to my comment, I think when is ok with the extra indenting becuase it is less common to be nested and can often pattern match on multiple things to remove nesting. I still would prefer it not to nest so much though (don't know of a reasonable syntax to help with that though).
if on the other hand is much more likely to nest and has no good solutions for removing nesting.
@Brendan Hansknecht Re extra indentation....I guess it's True (as proposed). I guess there's no reason we couldn't eliminate the indentaion:
We could change your example:
addBytes : LowLevelHasher, List U8 -> LowLevelHasher
addBytes = \@LowLevelHasher { originalSeed, state }, list ->
length = List.len list
seed = Num.bitwiseXor originalSeed wyp0
abs =
if
length <= 16 ->
if
length >= 4 ->
x = Num.shiftRightZfBy length 3 |> Num.shiftLeftBy 2
a = Num.bitwiseOr (wyr4 list 0 |> Num.shiftLeftBy 32) (wyr4 list x)
b =
(wyr4 list (Num.subWrap length 4) |> Num.shiftLeftBy 32)
|> Num.bitwiseOr (wyr4 list (Num.subWrap length 4 |> Num.subWrap x))
{ a, b, seed }
length > 0 ->
{ a: wyr3 list 0 length, b: 0, seed }
_ ->
{ a: 0, b: 0, seed }
length <= 48 ->
hashBytesHelper16 seed list 0 length
_ ->
hashBytesHelper48 seed seed seed list 0 length
combineState (@LowLevelHasher { originalSeed, state }) { a: abs.a, b: abs.b, seed: abs.seed, length: Num.toU64 length }
to:
addBytes : LowLevelHasher, List U8 -> LowLevelHasher
addBytes = \@LowLevelHasher { originalSeed, state }, list ->
length = List.len list
seed = Num.bitwiseXor originalSeed wyp0
abs =
if
length <= 16 ->
if
length >= 4 ->
x = Num.shiftRightZfBy length 3 |> Num.shiftLeftBy 2
a = Num.bitwiseOr (wyr4 list 0 |> Num.shiftLeftBy 32) (wyr4 list x)
b =
(wyr4 list (Num.subWrap length 4) |> Num.shiftLeftBy 32)
|> Num.bitwiseOr (wyr4 list (Num.subWrap length 4 |> Num.subWrap x))
{ a, b, seed }
length > 0 ->
{ a: wyr3 list 0 length, b: 0, seed }
_ ->
{ a: 0, b: 0, seed }
length <= 48 ->
hashBytesHelper16 seed list 0 length
_ ->
hashBytesHelper48 seed seed seed list 0 length
combineState (@LowLevelHasher { originalSeed, state }) { a: abs.a, b: abs.b, seed: abs.seed, length: Num.toU64 length }
A similar thing could be done with when..is (shown as if..is)
text =
if word is
"YES" ->
Stdout.line "word is: YES"
"NO ->
Stdout.line "word is: NO"
_ ->
Stdout.line "word is not YES or NO"
I actually prefer Brendan's "with proposed syntax:" example
@Anton Why? It does involve more indentation.
putting the length <= 16 -> at the same column as the if seems very atypical, I don't think I've seen anything like it in any programming language. The extra indentation makes it less busy.
I also generally like to pull functions out when things get too involved / deeply nested
@anton. Isn't that basically what being done with if..else to get the indentation that @Brendan Hansknecht prefers.
i.e. the natural indentation of an if statement is elided. I.e. normal if..else statements don't follow a rule of immediate indentation.
I'm sorry, I can't quite follow your statement, can you explain in more detail?
The traditional form of an if statement doesn't have every line that follows the start of the if indented.
Yes, I like the "next inside thing" to be indented
I think the proposed suggestion is to start an if with an initial indention and then indent further.
yes that's how I like it :)
Anton said:
Yes, I like the "next inside thing" to be indented
Since that's not how an if else statement works there's necessarily an extra level of indentation then right?
So someone could validly argue that the proposed if syntax causes extra indentation, and that that's a bad thing.
I don't mind the extra indentation in this case:
abs =
if
length <= 16 ->
if
length >= 4 ->
x = Num.shiftRightZfBy length 3 |> Num.shiftLeftBy 2
a = Num.bitwiseOr (wyr4 list 0 |> Num.shiftLeftBy 32) (wyr4 list x)
b =
(wyr4 list (Num.subWrap length 4) |> Num.shiftLeftBy 32)
|> Num.bitwiseOr (wyr4 list (Num.subWrap length 4 |> Num.subWrap x))
{ a, b, seed }
length > 0 ->
{ a: wyr3 list 0 length, b: 0, seed }
_ ->
{ a: 0, b: 0, seed }
length <= 48 ->
hashBytesHelper16 seed list 0 length
_ ->
hashBytesHelper48 seed seed seed list 0 length
combineState (@LowLevelHasher { originalSeed, state }) { a: abs.a, b: abs.b, seed: abs.seed, length: Num.toU64 length }
@Anton Either way is fine I guess, but I don't have any trouble parsing this:
text =
if word is
"YES" ->
Stdout.line "word is: YES"
"NO ->
Stdout.line "word is: NO"
_ ->
Stdout.line "word is not YES or NO"
I think it might look a bit more grounded, and practically it does remove one level of extra indentaion.
That particular example I gave above is pretty difficult to misconstrue isn't it?
I don't have any problems parsing it either and I bet I could get used to it easily. It's just seems so different than usual
If I'd seen it before I'd probably say it's the best way :)
I'm curious what others think
Honest question...why does it seem more ok for if .. else not to be written as:
if b <= 0 then
"c'mon..."
else if b == 1 then
"just one?"
else if b > 5 then
"whoa there"
else
"fine I guess"
Maybe because there is a keyword prefixing it?...makes it seem normal?
That indentation looks wild, "more ok" compared to what?
It seems like what you want to do is indent and then indent again, but that's not what if..else does so I'm trying to understand why it's acceptable for the if and else to all line up on the left but for the new if syntax not to.
Maybe it doesn't work normally because putting the if directly in line with the cases assumes that you are at a new level of indentation and nothing can follow at that level, that probably makes it wierd? (I'm still thinking about this:)
text =
if word is
"YES" ->
Stdout.line "word is: YES"
"NO ->
Stdout.line "word is: NO"
_ ->
Stdout.line "word is not YES or NO"
"because putting the if directly in line with the cases assumes that you are at a new level of indentation and nothing can follow at that level, that probably makes it weird" yes that was my feeling indeed
I'm trying to find an example where that wouldn't work.
Assuming that wraping the conditions in the elsekeyword permits the eliding of indentation...that probably brings us to either accepting the extra indentation or using a prefix character (like fsharp uses the pipe) or we're back to having a word prefix for the branches.
@Anton i have a few issue with the extra indentation. For example, in the code I write above, i would not feel safe pulling things into an extra function to avoid indentation (at least not without an always inline attribute being added to roc). Those branches are set up in a very specific way for performance.
That said, i also think, a lot of the time, more context is very important (and the main reason to avoid sub functions). So pulling into a lot of smaller function with only one use can really hurt readability. If you think a function should roughly be at most 4 indents deep (arbitrary number, just pretend this is when you would likely pull out a sub function), with the new syntax, you will be making new functions when you have half the amount of nesting as the old syntax. This means more cases of split up control flow where you can't see the full context and better hope you have a good name for the subfunctions that is understandable to others.
fsharp has a prefix character and doesn't indent after the match keyword:
let rangeTest testValue mid size =
match testValue with
| var1 when var1 >= mid - size/2 && var1 <= mid + size/2 -> printfn "The test value is in range."
| _ -> printfn "The test value is out of range."
addBytes = \@LowLevelHasher { originalSeed, state }, list ->
length = List.len list
seed = Num.bitwiseXor originalSeed wyp0
abs =
if
length <= 16 ->
if
length >= 4 ->
x = Num.shiftRightZfBy length 3 |> Num.shiftLeftBy 2
a = Num.bitwiseOr (wyr4 list 0 |> Num.shiftLeftBy 32) (wyr4 list x)
b =
(wyr4 list (Num.subWrap length 4) |> Num.shiftLeftBy 32)
|> Num.bitwiseOr (wyr4 list (Num.subWrap length 4 |> Num.subWrap x))
{ a, b, seed }
length > 0 ->
{ a: wyr3 list 0 length, b: 0, seed }
_ ->
{ a: 0, b: 0, seed }
length <= 48 ->
hashBytesHelper16 seed list 0 length
_ ->
hashBytesHelper48 seed seed seed list 0 length
the example above with proper vertical spacing
I think it also looks a bit weird due to the immediate nested if statement... so trying to refactor:
@Georges Boris
How about:
reaction =
if
| b <= 0 ->
"c'mon..."
| b == 1 ->
"just one?"
| b > 5 ->
"whoa there"
| _ ->
"fine I guess"
To reduce the amount of indentation needed (to avert the criticism that this causes too much indentation)?
addBytes = \@LowLevelHasher { originalSeed, state }, list ->
length = List.len list
seed = Num.bitwiseXor originalSeed wyp0
abs =
if
length <= 16 && length >= 4 ->
x = Num.shiftRightZfBy length 3 |> Num.shiftLeftBy 2
a = Num.bitwiseOr (wyr4 list 0 |> Num.shiftLeftBy 32) (wyr4 list x)
b =
(wyr4 list (Num.subWrap length 4) |> Num.shiftLeftBy 32)
|> Num.bitwiseOr (wyr4 list (Num.subWrap length 4 |> Num.subWrap x))
{ a, b, seed }
length > 0 && length >= 16 ->
{ a: wyr3 list 0 length, b: 0, seed }
length >= 16 ->
{ a: 0, b: 0, seed }
length <= 48 ->
hashBytesHelper16 seed list 0 length
_ ->
hashBytesHelper48 seed seed seed list 0 length
Refactoring is wrong in this case
It is that way specifically for branch prediction and performance
So just pretend they are unique branches that don't make sense to merge.
Also, i like the idea of a precursor character to avoid indenting
speaking of the match - what is the fundamental difference between pattern matching and if-else? maybe it's a bit out of the topic - but when I learned about pattern matching - my first question was "why do I need if-else now?"
they seem to do almost the same but have different wording. I didn't think about it much though
There is no fundamental difference, everything you can do with if you can do with a when match, if just optimizes space for the exactly-2-branches case
personally I feel like I write conditions like
prefix = if startOfLine then "header > " else ""
"\(prefix)(content)"
all the time, I don't think splitting conditions like that over multiple lines or indenting helps readability
@Kiryl Dziamura when..if (roc's) matching could be written as when True is followed by the conditional branches to produce an if-else system.
@Kiryl Dziamura we've discussed that previously and tried it out different ways but basically the if ignores a particular matched value and can match on different expressions that are independent of each otherm so basically:
when a is
_, 1 > 2 -> false
_, k == "f" -> true
we're ignoring the a and just using the guards.
I'm not a fan of the prefix character fwiw - that syntax is usually used for pattern match as a whole, right? and imo is quite verbose... but if it should be used for if then it should probably be used for when
Georges Boris said:
I'm not a fan of the prefix character fwiw - that syntax is usually used for pattern match as a whole, right? and imo is quite verbose... but if it should be used for
ifthen it should probably be used forwhen
in that case it would reduce indentation with both
I'm not seeing another way of competing with the traditional if else in terms of indentaion.
we can use 2-space indents if horizontal space is a priority :troll:
Are 4 spaces standard now?
or 3?
4 spaces. similar to Elm.
So it seems like we can have the proposed syntax but we have to accept extra indentation. (not sure how to decide that.).
OR
Have a prefix character on branches.
OR
Go back to if else
OR ..?
I'm definitely not sold on this being a good use of weirdness budget
if / then / else is not weird to anyone with a background in any mainstream language, but the proposed syntax is...and I'm just not seeing the benefits justifying that
supposing it is better, it's not by much
note that
when a is
_ if 1 > 2 -> false
_ if k == "f" -> true
is already valid syntax (it will need a default branch though). I really don't see the problem that is being solved here though.
(aside: you may see the above in some haskell code, because a pattern match strictly evaluates its scrutinee)
On the other hand there doesn't seem to be a real justification for having a completely different syntax for if else vs when is it's like they come from two different worlds by syntax even though they are intimately related. And the only reason we can't be more consistent and clear and elegant is because that's the way it's always been done and we can't figure out how to avoid increasing indentation without adding a prefix character. We already basically have this syntax, it's just a question of whether we want two disconnected syntaxes, one for if else and one for when is.
the proposed syntax puts the conditions front and center with minimal noise. The problem is syntactic noise. And we can see that it (the noise) doesn't exist in an existing structure (the when ..is) which is basically a superset of if else (redundancy).
yeah I'm fine with that redundancy
Does that mean this issue is decided or how does this work?
The way I see it, the proposed syntax has these advantages:
when..is syntaxAnd it has these downsides:
if/then/elseif is one of the most basic constructs of a programming language, and using an unfamiliar syntax is another barrier for newcomers. At least for me, that justifies having different syntaxes for if and when..is.
This seems well-suited to using an editor plugin to display it how you prefer :)
I appreciate that there are some others out there with my leanings. I would be very happy for something to happen with this proposal... My initial hope today was to suggest a way to reduce the noisiness of else if by changing that to elif. I really liked the essence of this proposal. But if the proposal of this thread is a no-go then I hope the idea of elif will be considered: https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/elif.20syntax
Agus Zubiaga said:
The way I see it, the proposed syntax has these advantages:
- It more closely resembles the
when..issyntax- Conditions line up when you have more than one
And it has these downsides:
- It prevents single-line if
- Adds a new level of indentation
- Adds a new line
- It's harder to read than the almost plain English:
if/then/elseifis one of the most basic constructs of a programming language, and using an unfamiliar syntax is another barrier for newcomers.At least for me, that justifies having different syntaxes for
ifandwhen..is.
advantages:
else or if if you decide to check for a condition before or after (same thing we usually get with trailing commas).as for the downsides:
I don't think single line if/else are usually used. I'm not sure if Roc is thinking differently but Elm always formats if's using multi-line and one can argue that if x > 2 then a else b is kinda noisy. it is not like a ternary operator.
about the extra line. I don't think vertical spacing should be a concern. Elm (and Roc) are languages that tend to be quite line-count heavy due to use of white space. never heard anyone see that as pain point on Elm codebases.
if/then if/do if {} - the if is always there, everything else related to an if statement is language specific and I really don't think the proposed syntax is hard to understand especially for Roc programmers which are already using when/is. We are already doing things that are way more alien, like bools (and I'm for that)
if we wanna give Roc users a choice between readability/more white space and terseness/less white space, maybe that could be something the formatter would be able to detect and work around that? With elm-format you may keep something inline or break it, the editor will respect that and format it the best it can for each case. So (trying to fight for this pitch a little more) - something like if a > b -> true, _ -> false could be achieved? This discussion could be separated tho...
I for one would love to choose between:
when a is
Good -> "good"
Bad -> "bad"
and
when a is
Good ->
"good"
Bad ->
"bad"
this adds up a lot when mapping values.
I've liked single-line if and the formatter in Roc supports them (I explicitly wanted to support them because I missed them in Elm!)
This is definitely an interesting debate. I find the weirdness budget a complex topic. I do think that proposed syntax in a lot of cases could be much nicer.
Last updated: Jun 16 2026 at 16:19 UTC