Sam Mohr said:
I think allowing early returns is preferable for writing clean code. AFAIK we have a way to use ! in
whenandifstatements so that you don't have to pull out intermediate vars
I'd say that early returns are exactly anif-elsestatement with sugar
Brendan Hansknecht said:
Fair enough. As long as you have an
elsewith a block that includes everything else, that is an early return kinda.
what if the formatter allowed single-line if/then/else without indentation afterwards, because avoiding that indentation is most of what's nice about "early return" in languages with a return keyword?
foo = \list ->
if List.isEmpty list then [] else
when List.len list is
...
Nice loophole, that could work
I like the idea! It reuses existing syntax and gives the same benefit of avoiding indentation. The only thing I think this doesn't handle is inline complex early return values. If I need a few lines to calculate my early return value, then either the line gets much longer, or we now need to use the normal if-else indentation.
yeah that's true, although personally I'm okay with having to indent in that case
It could be argued that it's good we need to use the normal if-else indentation in those cases, as they're complex enough that we want to avoid putting it all together
Yeah, agreed
If we do this, I'd vote that we require a newline under these for readability, it's a lot of keywords on one line right next to another otherwise.
the situation where the indentation annoys me is where I have a bunch of logic in the function and it's all indented just because at the very beginning I want to handle one obvious "don't do any of this if something is zero" base case
Precisely
I'll wait for other comments, and once other people weigh in, I can make an issue. This would actually make a good beginner issue!
foo = \list ->
if List.isEmpty list then
[]
else
when List.len list is
Personally I like this a bit more, but I like the general idea. Like you said, a simple base case function can get clearer this way
a variation on that idea: "indent the way a multiline early return would be indented"
foo = \list ->
if List.isEmpty list then
baz 42 blah etc
else
when List.len list is
I feel like a lot of these could be easy to miss. That will lead to debugging confusions. This lastest variant is much more likely to be noticed, which I think is important.
It seems easy enough to implement, too: if there's no indent on the else, proceed as normal. If the else is at the same indentation as the block, then force a newline and allow no indentation for the following block
That's my favorite one!
https://github.com/roc-lang/roc/issues/7039
Wow I literally wrote up most of a proposal for this exact thing last night but just hadn't published it yet :sweat_smile:
What if we use this syntax for early returns? I think it is clear and also familiar from many other languages.
fun = \x, y ->
if x == 0 then
100
x + y
I haven't had a chance to catch up on zulip yet so please correct me if I'm missing some relevant context
The options with else still included look a bit weird to me
Would you be okay with this?
fun = \x, y ->
if x == 0 then
100
x + y
The reason why the else options are more appealing are that they are just different "shapes" of existing syntax. If we accept this, which is somewhat of a new syntax, do we allow it elsewhere?
Do you mean would I be okay with requiring a newline or okay with the presence of a newline? I would definitely be fine with there being a newline, but it seems odd to require one.
Sam Mohr said:
Would you be okay with this?
fun = \x, y -> if x == 0 then 100 x + y
I would be for this syntax with the required newline as the default formatting
Also requiring the 100 to be on the next line
Otherwise, I think it is too easy to miss
I think that normally the else of the if-else block acts as the newline does, and separates the two bodies of the clause. Without the newline, it visually bleeds together. Close reading wouldn't miss it, but scanning code could definitely miss it
Okay, I think @Brendan Hansknecht 's suggestion is better in my eyes. If you like it @Isaac Van Doren I'll update the GitHub issue.
Yeah I'm fine with requiring a newline! And I agree, 100 should definitely be on the next line
Cool!
something I don't like about the lack of else is that it's not clear to me what this version does when it's written in other places than the very beginning of the function
else makes that very clear I think
else definitely makes that very clear, but I think the following is still clear:
giveCostForResult = \res ->
when res is
Err _err -> 0
Ok val ->
if val > 3 then
20
val
|> Num.pow 2
|> Num.div 3
Though not as clear, I agree.
It's not very FP, but a return keyword would also make it very clear
I think return would make it more confusing. Cause it would feel like it is returning from the function as a whole not just from the inner statement
Oh, very true
Yes, let's not do that
As an aside, if we do go with the syntax without the else, I think we should make the parser accept this:
fun = \x, y ->
if x == 0 then
100
x + y
but then have the formatter transform it to this:
fun = \x, y ->
if x == 0 then
100
x + y
I could see new users being surprised if the presence of that newline made a difference in the validity of the syntax.
Totally agree!
Yeah, I think we should only ban single line if without else
That is too confusing and easy to miss
Also, o would be fine with requiring the else, but I think it only looks worse and doesn't really help most of the time. It is implicit else syntax
Yeah, it seems like we just have to bully @Richard Feldman into agreeing /s
:laughing:
,--.--._
------" _, \___)
/ _/____)
\//(____)
------\ (__)
`-----"
I'm concerned that it'll stop being obvious if we introduce "if without else" in other places, and we keep accumulating motivating use cases for that
In that you're concerned about there being different meanings between the "early return" else-less if and the "otherwise null" else-less if?
like for example if we start allowing that for "statements" which run an effect but don't produce any value, we actively wouldn't want it to early return there
but it would be syntactically ambiguous which you meant
Yeah, for that reason I'm okay with keeping the else for early returns
Yeah, this conflicts with the other proposal
which requires elseless-if because there’s no way to do Task.ok {}
Oh, yeah, doesn't pair with !. Though really bang is the non-functional syntax. This is closer to properly functional
Richard Feldman said:
a variation on that idea: "indent the way a multiline early return would be indented"
foo = \list -> if List.isEmpty list then baz 42 blah etc else when List.len list is
sounds like we're sticking with else
Kilian Vounckx said:
What about this? It's analogous to else if...
foo = \list ->
if List.isEmpty list then
[]
else when List.len list is
I dont think this is about else followed by when specifically. It is about else followed by anything at all
By the way, the agreed on syntax has already been implemented! https://github.com/roc-lang/roc/pull/7060
Whoa awesome! Will have to update some code :nerd:
Oh wow....I'll have to mess around with that
It's pretty convenient
Side note, this implies I'll be wanting to think of making announcements when I merge contributions on features like these.
I really like this feature. The code looks so much nicer. This was a great idea.
Looks like I've found a bug in the formatting for early returns. I have a case where an assignment using if/then/else is being formatted with a hanging else as though it were an early return:
setModels : Client, List Str -> Client
setModels = \client, models ->
modelsOption = if List.isEmpty models
then Option.none {}
else Option.some models
{ client & models: modelsOption }
Causes a format failed bug, but gets formatted as the following in the debug output file:
setModels : Client, List Str -> Client
setModels = \client, models ->
modelsOption = if
List.isEmpty models
then
Option.none {}
else
Option.some models
{ client & models: modelsOption }
However, by moving the if onto a new line, formatting completes correctly.
setModels : Client, List Str -> Client
setModels = \client, models ->
modelsOption =
if List.isEmpty models
then Option.none {}
else Option.some models
{ client & models: modelsOption }
Issue filed: #7085
Interesting, I didn't even realize that was supported formatting
Ahh, sorry. I should have also included the output of the second format. That doesn't encounter a formatting failure error, but does format very awkwardly:
setModels : Client, List Str -> Client
setModels = \client, models ->
modelsOption =
if
List.isEmpty models
then
Option.none {}
else
Option.some models
{ client & models: modelsOption }
Haha, it is prioritizing the early return formatting
Also, that multiple if ... then is pretty gross
Personally, I like the if .. then formatting I had, as it is very similar to acceptable when formatting:
setMaxTokens : Client, U64 -> Client
setMaxTokens = \client, maxTokens ->
maxTokensOption =
when maxTokens is
0 -> Option.none {}
_ -> Option.some maxTokens
{ client & maxTokens: maxTokensOption }
setModels : Client, List Str -> Client
setModels = \client, models ->
modelsOption =
if List.isEmpty models
then Option.none {}
else Option.some models
{ client & models: modelsOption }
I like that formatting when each clause of the if expression is a single line, but once they become multiple lines things break down
sorry this is my mistake I authored #7060. I made the assumption that any multiline if would start on it's own line, so this just breaks it ig. It's also parsed as an early return whenever the else is indented past the if, that's why its prioritized here. Maybe it should only parse that way if else is its own separate line?
It's also on me, during testing I prioritized checking just if-else. Stuff happens!
Luckily, if the #ideas>`try` keyword instead of `?` suffix discussion really does lead to us adding a return keyword, this will be changed to a simpler syntax anyway:
processArgs = \args =>
if List.isEmpty args then
return Err NoArgs
# rest of body
and maybe that's a better way of doing although I thought this was very clever way of doing it
@sammi watt no need to apologize, and thank you for the contribution! :smiley:
nobody else anticipated that edge case either :big_smile:
@sammi watt at least you didn't merge someone else's big PR too early... :sweat_smile:
The contribution was well written and had tests, I still think you did a good job! Any contributor that puts the effort you did into that PR, we are happy to get help from.
actually I went back to see how it formatted it before and it still gives an error and outputs a file with this instead
setModels : Client, List Str -> Client
setModels = \client, models ->
modelsOption = if
List.isEmpty models
then
Option.none {}
else
Option.some models
{ client & models: modelsOption }
that's with roc --version
roc nightly pre-release, built from commit 2469a3a on Tue Sep 3 09:02:01 UTC 2024
Oof
Then it's even further underlying
I guess so :face_with_diagonal_mouth:
In which case, we should open a bug with a reproduction, preferably with and without the "early-return" syntax for maximum debug-ability.
I feel like this relates to the block parsing @Joshua Warner implemented. Just thought I'd mention in case there's an obvious fix
@sammi watt can you paste the error you're getting?
❯ roc format syntax.roc
An internal compiler expectation was broken.
This is definitely a compiler bug.
Please file an issue here: <https://github.com/roc-lang/roc/issues/new/choose>
Formatting bug; formatted code isn't valid
I wrote the incorrect result to this file for debugging purposes:
syntax.roc-format-failed
Parse error was: "Expr(Closure(Body(If(IndentThenToken(@130), @100), @85), @62), @9)"
Location: crates/cli/src/format.rs:101:21
syntax.roc-format-failed:
module []
setModels : Client, List Str -> Client
setModels = \client, models ->
modelsOption = if
List.isEmpty models
then
Option.none {}
else
Option.some models
{ client & models: modelsOption }
This comes from us formatting twice and seeing that the formatting wasn't idempotent, if I'm not mistaken
What the real cause is, I'm not sure
Parse error was: ... indicates that we formatted to something that we can't parse
Joshua Warner said:
sammi watt can you paste the error you're getting?
it was the same one as sam
Ahh so the issue here is that there is this unindented then inside the an assignment.
The assignment will require that all parts of the body thereof are indented more than the level of the assignment statement.
(the then needs to be indented past the modelsOption)
That said, there shouldn't be any possible ambiguities here for correct code, so we can probably safely relax that check.
Thank goodness we have you here @Joshua Warner ! I'm very happy we have someone that cares about managing parsing/syntax and is doing a good job at it, too
I'd love to get things in a state so others could come to the same conclusion just as quickly ;)
It's open-source, all good things take time
Indeed
I'm actually about to create another batch of syntax change issues
If there's anything I (or someone else) should know when getting into that to avoid making things worse in the future let me know
I can put that info into these issues
There are obviously lots of potential gotchas in the parser, but not anything we're I'm super concerned about people accidentally making it worse
great
It's handy to mention the fuzzer
Maybe also roc-osprey @Joshua Warner ??
That's not quite in a state where it's useful for others yet...
Ahk, well we better keep it a secret then :shushing_face: :wink:
"message redacted"
Pretty busy ATM, but I can give some guidance on fixing the issue above
I just need everyone to look into this 'neuralyzer' device...
There are two possible paths here:
modelsOption =, when there's an if there(1) should be pretty simple, and is IMO the way to go here. But it's a (minor) style change, so it's possible that opinions would vary.
(2) has a variety of possible approaches with different tradeoffs. The simplest and most direct thing would be to throw a reset_min_indent in if_branch (at the top level). But that'll mean relaxing indentation for all parts of the if _except_ the body of the else. That's _probably_ fine, but we'd need evaluate that carefully. Alternatively, we could intentionally just ignore indentation for then/else keywords, in which case we'd need to move space0_* calls to around the keywords and use a map_with_arena to re-attach them to the condition and then block exprs. Annoying, but possible.
Since we plan on introducing an actual early return in the form of a return keyword, I'm gonna make 2 GitHub issues, one to first add an if-return syntax, and then another to remove this syntax
I just put them together into a single GitHub issue: https://github.com/roc-lang/roc/issues/7105
Last updated: Jun 16 2026 at 16:19 UTC