Stream: ideas

Topic: "early returns" via formatter


view this post on Zulip Richard Feldman (Aug 28 2024 at 18:22):

Sam Mohr said:

I think allowing early returns is preferable for writing clean code. AFAIK we have a way to use ! in when and if statements so that you don't have to pull out intermediate vars
I'd say that early returns are exactly an if-else statement with sugar

Brendan Hansknecht said:

Fair enough. As long as you have an else with 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
        ...

view this post on Zulip Anton (Aug 28 2024 at 18:32):

Nice loophole, that could work

view this post on Zulip Sam Mohr (Aug 28 2024 at 18:51):

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.

view this post on Zulip Richard Feldman (Aug 28 2024 at 18:54):

yeah that's true, although personally I'm okay with having to indent in that case

view this post on Zulip Sam Mohr (Aug 28 2024 at 18:54):

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

view this post on Zulip Sam Mohr (Aug 28 2024 at 18:54):

Yeah, agreed

view this post on Zulip Sam Mohr (Aug 28 2024 at 18:54):

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.

view this post on Zulip Richard Feldman (Aug 28 2024 at 18:55):

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

view this post on Zulip Sam Mohr (Aug 28 2024 at 18:55):

Precisely

view this post on Zulip Sam Mohr (Aug 28 2024 at 18:55):

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!

view this post on Zulip Kilian Vounckx (Aug 28 2024 at 19:09):

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

view this post on Zulip Richard Feldman (Aug 28 2024 at 19:12):

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

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 19:15):

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.

view this post on Zulip Sam Mohr (Aug 28 2024 at 19:43):

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

view this post on Zulip Sam Mohr (Aug 28 2024 at 19:43):

That's my favorite one!

view this post on Zulip Sam Mohr (Aug 28 2024 at 20:26):

https://github.com/roc-lang/roc/issues/7039

view this post on Zulip Isaac Van Doren (Aug 28 2024 at 22:22):

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

view this post on Zulip Isaac Van Doren (Aug 28 2024 at 22:24):

The options with else still included look a bit weird to me

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:25):

Would you be okay with this?

fun = \x, y ->
    if x == 0 then
        100

    x + y

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:28):

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?

view this post on Zulip Isaac Van Doren (Aug 28 2024 at 22:29):

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.

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 22:30):

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

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 22:31):

Also requiring the 100 to be on the next line

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 22:31):

Otherwise, I think it is too easy to miss

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:32):

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

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:32):

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.

view this post on Zulip Isaac Van Doren (Aug 28 2024 at 22:34):

Yeah I'm fine with requiring a newline! And I agree, 100 should definitely be on the next line

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:34):

Cool!

view this post on Zulip Richard Feldman (Aug 28 2024 at 22:35):

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

view this post on Zulip Richard Feldman (Aug 28 2024 at 22:35):

else makes that very clear I think

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:38):

else definitely makes that very clear, but I think the following is still clear:

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:39):

giveCostForResult = \res ->
    when res is
        Err _err -> 0
        Ok val ->
            if val > 3 then
                20

            val
            |> Num.pow 2
            |> Num.div 3

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:40):

Though not as clear, I agree.

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:41):

It's not very FP, but a return keyword would also make it very clear

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 22:45):

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

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:46):

Oh, very true

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:46):

Yes, let's not do that

view this post on Zulip Isaac Van Doren (Aug 28 2024 at 22:50):

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.

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:50):

Totally agree!

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 22:50):

Yeah, I think we should only ban single line if without else

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 22:51):

That is too confusing and easy to miss

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 22:52):

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

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:54):

Yeah, it seems like we just have to bully @Richard Feldman into agreeing /s

view this post on Zulip Isaac Van Doren (Aug 28 2024 at 22:54):

:laughing:

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:55):

       ,--.--._
------" _, \___)
        / _/____)
        \//(____)
------\     (__)
       `-----"

view this post on Zulip Richard Feldman (Aug 28 2024 at 22:55):

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

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:56):

In that you're concerned about there being different meanings between the "early return" else-less if and the "otherwise null" else-less if?

view this post on Zulip Richard Feldman (Aug 28 2024 at 22:56):

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

view this post on Zulip Richard Feldman (Aug 28 2024 at 22:56):

but it would be syntactically ambiguous which you meant

view this post on Zulip Sam Mohr (Aug 28 2024 at 22:56):

Yeah, for that reason I'm okay with keeping the else for early returns

view this post on Zulip Agus Zubiaga (Aug 28 2024 at 22:57):

Yeah, this conflicts with the other proposal

view this post on Zulip Agus Zubiaga (Aug 28 2024 at 22:57):

which requires elseless-if because there’s no way to do Task.ok {}

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 22:58):

Oh, yeah, doesn't pair with !. Though really bang is the non-functional syntax. This is closer to properly functional

view this post on Zulip Sam Mohr (Aug 28 2024 at 23:01):

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

view this post on Zulip Kevin Gillette (Sep 13 2024 at 14:28):

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

view this post on Zulip Brendan Hansknecht (Sep 13 2024 at 15:36):

I dont think this is about else followed by when specifically. It is about else followed by anything at all

view this post on Zulip Sam Mohr (Sep 13 2024 at 15:51):

By the way, the agreed on syntax has already been implemented! https://github.com/roc-lang/roc/pull/7060

view this post on Zulip Isaac Van Doren (Sep 13 2024 at 15:55):

Whoa awesome! Will have to update some code :nerd:

view this post on Zulip Brendan Hansknecht (Sep 13 2024 at 15:57):

Oh wow....I'll have to mess around with that

view this post on Zulip Sam Mohr (Sep 13 2024 at 16:01):

It's pretty convenient

view this post on Zulip Sam Mohr (Sep 13 2024 at 16:02):

Side note, this implies I'll be wanting to think of making announcements when I merge contributions on features like these.

view this post on Zulip Oskar Hahn (Sep 16 2024 at 19:13):

I really like this feature. The code looks so much nicer. This was a great idea.

view this post on Zulip Ian McLerran (Sep 17 2024 at 19:25):

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

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 19:37):

Interesting, I didn't even realize that was supported formatting

view this post on Zulip Ian McLerran (Sep 17 2024 at 19:39):

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 }

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 19:45):

Haha, it is prioritizing the early return formatting

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 19:45):

Also, that multiple if ... then is pretty gross

view this post on Zulip Ian McLerran (Sep 17 2024 at 19:51):

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 }

view this post on Zulip Isaac Van Doren (Sep 18 2024 at 19:55):

I like that formatting when each clause of the if expression is a single line, but once they become multiple lines things break down

view this post on Zulip sammi watt (Sep 18 2024 at 22:27):

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?

view this post on Zulip Sam Mohr (Sep 18 2024 at 22:29):

It's also on me, during testing I prioritized checking just if-else. Stuff happens!

view this post on Zulip Sam Mohr (Sep 18 2024 at 22:30):

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

view this post on Zulip sammi watt (Sep 18 2024 at 22:41):

and maybe that's a better way of doing although I thought this was very clever way of doing it

view this post on Zulip Richard Feldman (Sep 18 2024 at 22:42):

@sammi watt no need to apologize, and thank you for the contribution! :smiley:

view this post on Zulip Richard Feldman (Sep 18 2024 at 22:42):

nobody else anticipated that edge case either :big_smile:

view this post on Zulip Sam Mohr (Sep 18 2024 at 22:45):

@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.

view this post on Zulip sammi watt (Sep 18 2024 at 22:50):

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 }

view this post on Zulip sammi watt (Sep 18 2024 at 22:50):

that's with roc --version

roc nightly pre-release, built from commit 2469a3a on Tue Sep  3 09:02:01 UTC 2024

view this post on Zulip Sam Mohr (Sep 18 2024 at 22:52):

Oof

view this post on Zulip Sam Mohr (Sep 18 2024 at 22:52):

Then it's even further underlying

view this post on Zulip sammi watt (Sep 18 2024 at 22:55):

I guess so :face_with_diagonal_mouth:

view this post on Zulip Sam Mohr (Sep 18 2024 at 22:56):

In which case, we should open a bug with a reproduction, preferably with and without the "early-return" syntax for maximum debug-ability.

view this post on Zulip Luke Boswell (Sep 18 2024 at 23:00):

I feel like this relates to the block parsing @Joshua Warner implemented. Just thought I'd mention in case there's an obvious fix

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:12):

@sammi watt can you paste the error you're getting?

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:13):

❯ 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

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:13):

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 }

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:14):

This comes from us formatting twice and seeing that the formatting wasn't idempotent, if I'm not mistaken

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:14):

What the real cause is, I'm not sure

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:16):

Parse error was: ... indicates that we formatted to something that we can't parse

view this post on Zulip sammi watt (Sep 18 2024 at 23:19):

Joshua Warner said:

sammi watt can you paste the error you're getting?

it was the same one as sam

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:19):

Ahh so the issue here is that there is this unindented then inside the an assignment.

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:20):

The assignment will require that all parts of the body thereof are indented more than the level of the assignment statement.

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:20):

(the then needs to be indented past the modelsOption)

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:21):

That said, there shouldn't be any possible ambiguities here for correct code, so we can probably safely relax that check.

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:21):

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

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:22):

I'd love to get things in a state so others could come to the same conclusion just as quickly ;)

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:22):

It's open-source, all good things take time

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:22):

Indeed

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:23):

I'm actually about to create another batch of syntax change issues

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:23):

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

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:23):

I can put that info into these issues

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:24):

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

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:24):

great

view this post on Zulip Luke Boswell (Sep 18 2024 at 23:25):

It's handy to mention the fuzzer

view this post on Zulip Luke Boswell (Sep 18 2024 at 23:25):

Maybe also roc-osprey @Joshua Warner ??

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:26):

That's not quite in a state where it's useful for others yet...

view this post on Zulip Luke Boswell (Sep 18 2024 at 23:26):

Ahk, well we better keep it a secret then :shushing_face: :wink:

view this post on Zulip Sam Mohr (Sep 18 2024 at 23:27):

"message redacted"

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:28):

Pretty busy ATM, but I can give some guidance on fixing the issue above

view this post on Zulip Luke Boswell (Sep 18 2024 at 23:28):

I just need everyone to look into this 'neuralyzer' device...

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:29):

There are two possible paths here:

  1. change the formatter to force a newline + indent after modelsOption =, when there's an if there
  2. Relax (well, remove) the indentation requirements when parsing spaces before 'then' and 'else' keywords.

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:30):

(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.

view this post on Zulip Joshua Warner (Sep 18 2024 at 23:35):

(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.

view this post on Zulip Sam Mohr (Sep 19 2024 at 20:51):

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

view this post on Zulip Sam Mohr (Sep 19 2024 at 22:11):

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