Stream: ideas

Topic: opting out of short-circuiting


view this post on Zulip Joe Ennis (Sep 07 2024 at 16:26):

Could you have a keyword suppress the short circuit, like when handle runEffect! {} is… which could be used outside of a when as well? As someone learning the language it feels like when turns into this “specially privileged, only in certain cases, short-circuit suppressor”, and not that ! behaves differently in different contexts. when feels like it’s doing too much, both maybe-suppressing and pattern matching.

view this post on Zulip Joe Ennis (Sep 07 2024 at 16:48):

"! short-circuits, use handle to prevent this" seems easier to explain and/or understand

view this post on Zulip Notification Bot (Sep 07 2024 at 17:15):

2 messages were moved here from #ideas > Purity inference proposal v3 by Richard Feldman.

view this post on Zulip Richard Feldman (Sep 07 2024 at 17:15):

this is an interesting idea!

view this post on Zulip Richard Feldman (Sep 07 2024 at 17:16):

one thing to consider is how ? would work in conditionals

view this post on Zulip Richard Feldman (Sep 07 2024 at 17:17):

for example:

when validateEmail (Str.fromUtf8? bytes) is
    Ok email -> ...
    Err InvalidEmail -> ...
    Err BadUtf8 -> ...

view this post on Zulip Richard Feldman (Sep 07 2024 at 17:18):

this example works because short-circuiting doesn't escape the when condition

view this post on Zulip Richard Feldman (Sep 07 2024 at 17:20):

I guess there's an argument that if I changed it to validateEmail? then I expect it to make the entire when short-circuit because I don't want to match on a Result

when validateEmail? (Str.fromUtf8? bytes) is
    "foo@example.com" -> ...
    _ -> ...

view this post on Zulip Richard Feldman (Sep 07 2024 at 17:26):

whereas in the v3 proposal the validateEmail? wouldn't do anything differently

view this post on Zulip Joe Ennis (Sep 07 2024 at 17:36):

It feels weird that you're not able to refactor that original example with a emailStr = Str.fromUtf8? bytes followed by when validateEmail emailStr is... without the behavior changing. I think "short-circuiting" as a concept is a little easier to understand when it is "early returning" as opposed to being "being captured by some boundary, worry about knowing those boundaries" - though this is all from the brain of someone new to functional programming / roc.

view this post on Zulip Joe Ennis (Sep 07 2024 at 17:56):

So I suppose I'm suggesting a manual boundary:

result = handle (validateEmail? (Str.fromUtf8? bytes))

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

Without having read this thread, this is the solution I thought of for when being the real special case. Let me get a thesaurus out and consider some alternative names

view this post on Zulip Notification Bot (Sep 07 2024 at 22:55):

13 messages were moved from this topic to #ideas > early return statement by Richard Feldman.

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:26):

setting aside considerations of how things work today, and effects, and #ideas > early return statement - just thinking about how we want ? to work, should these work?

fn : List U8 -> Result U64 [BadUtf8 ...]
fn = \bytes ->
    num =
        if Str.fromUtf8? bytes == "stuff" then
            42
        else
            0

    dbg "after the condition"

    Ok num

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:26):

fn : List U8 -> Result U64 [InvalidNumStr, BadUtf8 ...]
fn = \bytes ->
    num =
        when Str.fromUtf8? bytes is
            "stuff" -> 42
            other -> Str.toU64? other

    dbg "after the condition"

    Ok num

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:29):

the first one has a ? in the condition, and in the example that short-circuits not just the expression between if and then but also the entire if expression itself, resulting in it short-circuiting all the way past the dbg (which wouldn't print anything) to the end of the function

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:29):

the second one has a ? in the condition and also a ? in one of the branches, and in that example it's the same thing: now that branch causes the entire when to short-circuit to the end of the function

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:30):

so I think in this design, the rules for ? are essentially "early return up to the nearest function or block of defs (so, sequence of one or more =s at the same indentation level), whichever is nearer"

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:33):

in a world where the second example shouldn't work, because the ? in the branch would be a no-op, then I think the rules would be:

"early return up to the nearest function or block of defs (so, sequence of one or more =s at the same indentation level), or conditional branch, whichever is nearest"

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:34):

and then if we wanted neither of the examples to work, then the rules would be:

"early return up to the nearest function or block of defs (so, sequence of one or more =s at the same indentation level), or conditional branch, or condition expression, whichever is nearest"

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:35):

That seems to make zero-sub def expressions return early, and sub def expressions not, if I understand correctly. For example:

solve = \a, b ->
    parsedA = Str.toU64? a
    parsedB =
        intermediate = Json.parse? b
        Str.toU64? intermediate

    Ok (parsedA * parsedB?)

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:35):

that's correct :thumbs_up:

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:37):

Hmmm, not huge on that rule difference, because I'm almost always deferring to a function boundary myself, but it's okay with me. It does seem a bit unobvious to new users, but they should intuit things pretty quickly after light usage

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:39):

That seems like it precludes the need for a try keyword, then.

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:41):

I think it would more clearly illustrate the distinction if parsedB didn't use ? at the end, e.g.

solve = \a, b ->
    parsedA : U64
    parsedA = Str.toU64? a

    resultB : Result U64 [InvalidNumStr]
    resultB =
        intermediate = Json.parse? b
        Str.toU64 intermediate

    parsedB : U64
    parsedB = resultB?

    Ok (parsedA * parsedB?)

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:43):

You're right, that's more clear! I'm not sure if people will end up putting ? after every fallible operation, we could add a warning preventing unnecessary ? usage

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:43):

But if they don't, then it's pretty obvious what's happening.

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:45):

yeah it would result in a type mismatch, but we could potentially show in the error message "hey this ? right here is causing this to not be the Result you seem to be expecting"

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:48):

hm actually I think my statements about the rules were incorrect

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:48):

in that the original examples don't just "short-circuit to the nearest block of defs" - because if you have a conditional where one of its branches is a nested conditional, and the nested one uses ?, it would only short-circuit up to the parent conditional, not all the way up to the defs

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:52):

Actually, I think it should be fine if the branches of conditionals work the exact same way as definitions with respect to ?

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:53):

If the conditional branch has no defs and just an expression, it propagates at the top level. If there are defs, then the whole branch is a result block

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:56):

Seems like a simple "blocks and functions" extension

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:56):

hm, so if I put this into the repl, what would I expect it to say? :thinking:

Str.concat (Str.fromUtf8? ['a', 'b', 'c']) "!"

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:56):

Well, the REPL... that's a bit different

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:56):

I think the answer is

Ok "abc!" : Result Str [BadUtf8]

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:56):

That should be it for the REPL, though I don't know if the REPL wraps everything in a function or a block

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:57):

but if that's the case, then I'd expect this to work too:

result : Result Str [BadUtf8]
result = Str.concat (Str.fromUtf8? ['a', 'b', 'c']) "!"

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:57):

yeah I'm more thinking about like "how it should work"

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:57):

than whether it fits the rules I said earlier, which I'm feeling less and less confident are correct :laughing:

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:58):

Well, I ask because the REPL is a weird case, right?

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:58):

I think it is currently, but it shouldn't be :sweat_smile:

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:58):

in the sense that if we're going for an expression-oriented ? then expressions should sort of "work normally"

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:59):

like if you put something into the repl and it works without giving an error, then that expression with that type should be portable to other places

view this post on Zulip Sam Mohr (Sep 08 2024 at 00:59):

Usually, our code looks like

main = \{} =>
    firstArg =
        args! {}
        |> List.first
        |> Result.withDefault ""
    res = Str.toU64? firstArg

    Stdout.line! "res is $(Num.toStr res)"

    Ok {}

view this post on Zulip Richard Feldman (Sep 08 2024 at 00:59):

so putting Str.fromUtf8? ['a', 'b', 'c'] on its own into the repl - I could see an argument for the ? being a no-op there, or for it giving an error

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:01):

yeah I'm trying to avoid ! here because I think it muddies the waters as far as what the short-circuiting rules should be

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

Maybe this is crazy, but I think the way that I'd want the REPL to work with ? is to unwrap at the "function and block" level, so Str.toU64? "123 should return 123

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:01):

but then what happens if you do Str.toU64? "not a number"

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:01):

in the repl

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:01):

crash?

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

It would "throw" an error, yeah

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

That's what I would want out of a REPL. "Just do the thing, I don't care"

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:02):

ehh I expect things I put into the repl to either give me a compile error or else I can copy/paste them out of the repl and into my code and they work the same way

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:02):

Well, with the new rules, if the REPL works like:

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:03):

main =
    Str.toU64? "abc"

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:03):

I think based on that I expect the repl to give me an eror if I put in Str.fromUtf8? ['a', 'b', 'c'] because it would say something like ? can only be used in nested expressions

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:03):

This should be a type error

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:03):

Yeah, I'd personally totally be okay with that

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:04):

The (maybe weird) exception to that is that I'd want ? to work in pipelines for zero-def expressions:

x =
    Cache.get "file.txt"
    |> Str.parse?
    |> Str.toU64

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:05):

If Str.parse and Str.toU64 are both fallible

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:05):

ok so in that world, does this become the rule?

"foo? desugars to the following:"

when foo is
    Ok val -> {{foo's parent expression, with foo replaced with val}}
    Err err -> Err err

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:05):

Huh, I'm not sure actually how I'd expect pipelines to work with these rules. I think for the REPL I'd expect Result.try

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:06):

I'm not sure if that's what we want, but it is at least a simple rule :sweat_smile:

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:07):

I think that's what we currently do by translating foo? bar into Result.try (foo bar) \fooOk -> ...

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:07):

and then I guess similarly, foo? bar baz means the same thing except it's applied to whatever foo evaluates to, because obviously if foo is a function then it's not a Result

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:08):

or to say it more simply, foo? bar baz is syntax sugar for (foo bar baz)?

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:11):

ok so desugaring that into the original examples, I think they both still work in terms of short-circuiting, (edit: never mind, looks like they don't!) which the second one alone illustrates:

Richard Feldman said:

fn : List U8 -> Result U64 [InvalidNumStr, BadUtf8 ...]
fn = \bytes ->
    num =
        when Str.fromUtf8? bytes is
            "stuff" -> 42
            other -> Str.toU64? other

    dbg "after the condition"

    Ok num

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:12):

applying the foo? bar baz desugaring to (foo bar baz)? we get:

fn : List U8 -> Result U64 [InvalidNumStr, BadUtf8 ...]
fn = \bytes ->
    num =
        when (Str.fromUtf8 bytes)? is
            "stuff" -> 42
            other -> (Str.toU64 other)?

    dbg "after the condition"

    Ok num

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:13):

and then applying the "replace the enclosing expression with a when we desugar this to:

fn : List U8 -> Result U64 [InvalidNumStr, BadUtf8 ...]
fn = \bytes ->
    num =
        when Str.fromUtf8 bytes is
            Ok val ->
                when val is
                    "stuff" -> 42
                    other -> (Str.toU64 other)?

            Err err -> Err err

    dbg "after the condition"

    Ok num

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:14):

hm yeah that doesn't work as expected

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:14):

because now num = ends up being a Result instead of a number

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:14):

We need some way to pass to the left of the =

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:15):

so we could propagate with a ? on the when

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:15):

e.g. if it was (when Str.fromUItf8 bytes is ..)?

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:15):

then it would propagate

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:15):

and we could have when? be syntax sugar for that:

fn : List U8 -> Result U64 [InvalidNumStr, BadUtf8 ...]
fn = \bytes ->
    num =
        when? Str.fromUtf8 bytes is
            Ok val ->
                when val is
                    "stuff" -> 42
                    other -> (Str.toU64 other)?

            Err err -> Err err

    dbg "after the condition"

    Ok num

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:16):

so that would desugar to making the entire surrounding def-expression be a when

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:17):

hm, another problem is that if other -> (Str.toU64 other)? short-circuits, the desugaring needs to add an Ok to all the other branches unless they also short-circuit

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:18):

because otherwise the wrapper when ... is Ok val ->won't work when given 42 instead fo Ok 42

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

That's a problem if this num = when ... turns into a result, but not if it turns into a Num

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:18):

this is just about this expression in isolation:

when val is
    "stuff" -> 42
    other -> (Str.toU64 other)?

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:19):

like if you put that into the repl

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:19):

either we need to require -> Ok 42 or else the sugar needs to add the Ok in the other branches

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:19):

or else this will be a type mismatch in the repl

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:19):

I think this is an "every step of the way" approach that make the "up to the nearest def or function" more appealing

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:22):

yeah, kind what I'm trying to do here is to figure out if we can get:

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

There are a few decisions juggling here:

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:23):

I think it's hard to beat early return on understandability of what the operator is doing, but I also enjoy expressions nesting and not affecting ancestor expressions, so I'm trying to see if there's a design we can find that does it all

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:24):

Yep!

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:24):

Yes, I think I'm unconsciously trying to convince you that my ruleset works, and you're trying out a different approach

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:25):

So I still think the "defs and functions" rule is the best I've seen that still preserves the expression nesting value

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:27):

Also, I think this isn't that bad, and allows the "defs and funcs" thing to work with when:

topRes =
    parseRes = Str.toU64? input
    when parseRes is
        123 -> foo? bar
        456 -> 789

this would be a Result U64 [...]

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:28):

It's more verbose than just allowing the inline ? in the matching expression of the when, but I expect the ? in the when is usually equivalent to a x = Str.toU64? s, a.k.a. early return

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:28):

Okay, I've said my piece

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:28):

the problem with the "defs and functions" rule is that if I nest like 4 conditionals within each others' branches, and I use a ? in a leaf condition, it short-circuits all the intervening ones too

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:29):

Yes, that's true

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:29):

so maybe I should say that one of the goals of the rule set would be that you always rewrite a fixed number of ancestor expressions

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:29):

otherwise I think early return is better :stuck_out_tongue:

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:30):

because the reason to not have early return is wanting to know that if I put this expression inside this other expression, they just nest and I know what that does

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:30):

and early return means "well actually this expression might jump control flow to multiple ancestor expressions"

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:30):

Makes sense

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:31):

but a problem I'm noticing with "only rewrite the immediate parent expression, not multiple ancestors" is that it means you need when? and if?

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:31):

Which I think is really unfortunate in the when? Str.toU64? in is case

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:32):

so another possible design is that it's the immediate parent expression, but there are exceptions - and if/when conditions are one of them

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:32):

in that design, if Str.fromUtf8? bytes == "abc" then is a type mismatch

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:33):

because now Str.fromUtf8? bytes == "abc" is a Result, not a Bool

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:33):

In that you basically can't use ? to propagate in the if input expr

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:33):

actually wait, I think that's the case in either design

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:33):

better example: if parseBool? input then

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:34):

in the "rewrite outer expression, that's it" design, that one works because the whole if expression gets wrapped in a when

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:34):

Yes

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:34):

but in the "...except inside if/when conditions" design, I guess the ? in parseBool? input becomes either a no-op or a compile error

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:35):

There's something to be said about preventing writing complex expressions in the if statement for readability

view this post on Zulip Sam Mohr (Sep 08 2024 at 01:36):

Not sure if that would lead to ifExpr = parseBool? input; if ifExpr then or more usefully-named vars...

view this post on Zulip Richard Feldman (Sep 08 2024 at 01:40):

yeah I suspect it would

view this post on Zulip Brendan Hansknecht (Sep 08 2024 at 02:41):

so another possible design is that it's the immediate parent expression, but there are exceptions - and if/when conditions are one of them

Yeah, something like this sounds reasonable to me

view this post on Zulip Sam Mohr (Sep 08 2024 at 02:42):

If we can find out the right way to handle if/when, then it's good to me as well

view this post on Zulip Brendan Hansknecht (Sep 08 2024 at 02:42):

For me I think the anchoring unit is blocks after an equal sign

view this post on Zulip Brendan Hansknecht (Sep 08 2024 at 02:43):

Those are what I think fundamentally need to block ! or ?

view this post on Zulip Brendan Hansknecht (Sep 08 2024 at 02:47):

Like when you have

someVar =
    # many lines of statements
    someFinalExpr

That should only ever propagate if some final expression has a ? Or ! directly in it


Last updated: Jun 16 2026 at 16:19 UTC