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.
"! short-circuits, use handle to prevent this" seems easier to explain and/or understand
2 messages were moved here from #ideas > Purity inference proposal v3 by Richard Feldman.
this is an interesting idea!
one thing to consider is how ? would work in conditionals
for example:
when validateEmail (Str.fromUtf8? bytes) is
Ok email -> ...
Err InvalidEmail -> ...
Err BadUtf8 -> ...
this example works because short-circuiting doesn't escape the when condition
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" -> ...
_ -> ...
whereas in the v3 proposal the validateEmail? wouldn't do anything differently
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.
So I suppose I'm suggesting a manual boundary:
result = handle (validateEmail? (Str.fromUtf8? bytes))
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
13 messages were moved from this topic to #ideas > early return statement by Richard Feldman.
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
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
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
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
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"
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"
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"
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?)
that's correct :thumbs_up:
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
That seems like it precludes the need for a try keyword, then.
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?)
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
But if they don't, then it's pretty obvious what's happening.
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"
hm actually I think my statements about the rules were incorrect
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
Actually, I think it should be fine if the branches of conditionals work the exact same way as definitions with respect to ?
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
Seems like a simple "blocks and functions" extension
hm, so if I put this into the repl, what would I expect it to say? :thinking:
Str.concat (Str.fromUtf8? ['a', 'b', 'c']) "!"
Well, the REPL... that's a bit different
I think the answer is
Ok "abc!" : Result Str [BadUtf8]
That should be it for the REPL, though I don't know if the REPL wraps everything in a function or a block
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']) "!"
yeah I'm more thinking about like "how it should work"
than whether it fits the rules I said earlier, which I'm feeling less and less confident are correct :laughing:
Well, I ask because the REPL is a weird case, right?
I think it is currently, but it shouldn't be :sweat_smile:
in the sense that if we're going for an expression-oriented ? then expressions should sort of "work normally"
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
Usually, our code looks like
main = \{} =>
firstArg =
args! {}
|> List.first
|> Result.withDefault ""
res = Str.toU64? firstArg
Stdout.line! "res is $(Num.toStr res)"
Ok {}
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
yeah I'm trying to avoid ! here because I think it muddies the waters as far as what the short-circuiting rules should be
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
but then what happens if you do Str.toU64? "not a number"
in the repl
crash?
It would "throw" an error, yeah
That's what I would want out of a REPL. "Just do the thing, I don't care"
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
Well, with the new rules, if the REPL works like:
main =
Str.toU64? "abc"
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
This should be a type error
Yeah, I'd personally totally be okay with that
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
If Str.parse and Str.toU64 are both fallible
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
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
I'm not sure if that's what we want, but it is at least a simple rule :sweat_smile:
I think that's what we currently do by translating foo? bar into Result.try (foo bar) \fooOk -> ...
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
or to say it more simply, foo? bar baz is syntax sugar for (foo bar baz)?
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
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
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
hm yeah that doesn't work as expected
because now num = ends up being a Result instead of a number
We need some way to pass to the left of the =
so we could propagate with a ? on the when
e.g. if it was (when Str.fromUItf8 bytes is ..)?
then it would propagate
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
so that would desugar to making the entire surrounding def-expression be a when
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
because otherwise the wrapper when ... is Ok val ->won't work when given 42 instead fo Ok 42
That's a problem if this num = when ... turns into a result, but not if it turns into a Num
this is just about this expression in isolation:
when val is
"stuff" -> 42
other -> (Str.toU64 other)?
like if you put that into the repl
either we need to require -> Ok 42 or else the sugar needs to add the Ok in the other branches
or else this will be a type mismatch in the repl
I think this is an "every step of the way" approach that make the "up to the nearest def or function" more appealing
yeah, kind what I'm trying to do here is to figure out if we can get:
return all the way to the function boundaryThere are a few decisions juggling here:
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
Yep!
Yes, I think I'm unconsciously trying to convince you that my ruleset works, and you're trying out a different approach
So I still think the "defs and functions" rule is the best I've seen that still preserves the expression nesting value
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 [...]
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
Okay, I've said my piece
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
Yes, that's true
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
otherwise I think early return is better :stuck_out_tongue:
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
and early return means "well actually this expression might jump control flow to multiple ancestor expressions"
Makes sense
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?
Which I think is really unfortunate in the when? Str.toU64? in is case
so another possible design is that it's the immediate parent expression, but there are exceptions - and if/when conditions are one of them
in that design, if Str.fromUtf8? bytes == "abc" then is a type mismatch
because now Str.fromUtf8? bytes == "abc" is a Result, not a Bool
In that you basically can't use ? to propagate in the if input expr
actually wait, I think that's the case in either design
better example: if parseBool? input then
in the "rewrite outer expression, that's it" design, that one works because the whole if expression gets wrapped in a when
Yes
but in the "...except inside if/when conditions" design, I guess the ? in parseBool? input becomes either a no-op or a compile error
There's something to be said about preventing writing complex expressions in the if statement for readability
Not sure if that would lead to ifExpr = parseBool? input; if ifExpr then or more usefully-named vars...
yeah I suspect it would
so another possible design is that it's the immediate parent expression, but there are exceptions - and
if/whenconditions are one of them
Yeah, something like this sounds reasonable to me
If we can find out the right way to handle if/when, then it's good to me as well
For me I think the anchoring unit is blocks after an equal sign
Those are what I think fundamentally need to block ! or ?
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