How will that syntax work with the purity inference idea? Would it only be available in functions that return Result {} _ and the else branch would be Ok {}? It seems a little bit less desirable now than when it was proposed in the context of ! because you can't use it if your function is effectful and doesn't return a result.
nvm I remember Richard told me you can do this in the purity inference proposal
if File.exists file then
File.delete file
else
{}
but elseless-if would be nicer of course
Isaac Van Doren said:
How will that syntax work with the purity inference idea? Would it only be available in functions that return
Result {} _and the else branch would beOk {}? It seems a little bit less desirable now than when it was proposed in the context of!because you can't use it if your function is effectful and doesn't return a result.
I think it should give you an “ignored result” warning if it returns anything other than {}
That’s probably something to discuss on the other topic, though
It seems like you wouldn't be able to use an elseless if it at all with a function that returns a result because otherwise the compiler would have to be able to determine if the else branch should return {} or Ok {}.
But yes this is definitely a different topic :smile:
I haven’t thought it through but I think you just need to add a ? to get rid of the Result
and if it’s still not {} and you want to ignore it, you have to do _ = …
If ? is still doing Result.try like it is today, then the else branch would need to return Ok {} in that case instead of {} wouldn't it?
Yes
In the new Task world, all effectful functions return a Result
Unless we change how Tasks work back to allowing non-Result values, which is probably the right idea
We needed them to be Result-like to make ! usable (I think), but that wouldn't be necessary anymore
in #ideas > Purity Inference the design is that Task no longer exists in userspace (although the data structure behind the scenes is the same), and effectfulness is totally decoupled from error handling
so, just like in Rust, if you want to opt into error handling, you introduce Result and Ok and ? and so on
and if the effect can't fail, you don't use Result in any way
(unlike today with Task)
That will actually help with some of this Task Str * cruft we're dealing with at the moment, since it'll now be Str and we won't need to do the Task.mapErr \_ -> crash "unreachable" workaround!
Off-topic, but whatever
Right, I'm just trying to understand how the "I want to conditionally do an effect without an else branch" syntax will work in the purity inference world. Like this:
foo = \{} ->
if Bool.true then
File.delete "file.txt"
Stdout.line "The file was deleted"
(note: this is not the previously proposed early return syntax but the conditional effect syntax :sweat_smile:)
yeah that would work the way you'd expect it to
the omitted else would be sugar for:
{} = if ... then ... else {}
so it adds else {} for you
So I couldn't use that syntax if File.delete returned a result instead?
You could do this, I think:
foo = \{} ->
if Bool.true then
File.delete? "file.txt"
Stdout.line "The file was deleted"
Ok {}
(assuming Stdout.line here doesn't return a Result)
yeah just like how Rust does it (in this case)
I just can't figure out how to desugar that to use Result.try :sweat_smile:
foo = \{} ->
if Bool.true then
File.delete? "file.txt"
else
Stdout.line "The file was deleted"
Ok {}
If you put it in this form, you'll notice that the File.delete? is the "return" of the function, so the ? is dropped, like how the last ! is dropped
That’s different, though. The Stdout.line needs to run in both branches.
I think this what it’d desugar to:
foo = \{} ->
Result.try
(if Bool.true then
File.delete "file.txt"
else
Ok {}
)
\{} ->
Stdout.line "The file was deleted"
Ok {}
Hmm, I guess this is why we need the else keyword, since I disagree about the desugaring
I think this what it’d desugar to:
Yeah that looks right to me.
Isn't it a problem that the compiler has to know that the else branch returns Ok {} instead of {}?
@Agus Zubiaga do you think my intermediate desugar above is wrong?
Yes because the Stdout.line only runs in one branch instead of both
I think what you wrote makes sense for an early return, but we are discussing elseless-if in the context of purity inference
Admittedly in the wrong Zulip topic, though :grinning:
Hmmmmm :thinking: :thinking: :thinking: :thinking:
Yes, if someone could move this to a different topic that would be great :smiley:
Maybe this needs to be discussed properly
A message was moved here from #ideas > "early returns" via formatter by Richard Feldman.
A message was moved from this topic to #ideas > the ? operator in Purity Inference design by Richard Feldman.
moved!
To hopefully clarify things, the question is how can both of these functions work simultaneously?
conditionalEffectThatReturnsResult : {} -> Result! {} _
conditionalEffectThatReturnsResult = \{} ->
Stdout.line "this is printed every time"
if condition then
effectThatReturnsAResult? {}
Stdout.line "This is printed if effectThatReturnsAResult returns an Ok"
Ok {}
conditionalEffectThatReturnsUnit : {} -> {}!
conditionalEffectThatReturnsUnit = \{} ->
Stdout.line "this is printed every time"
if condition then
effectThatReturnsUnit {}
Stdout.line "This is printed every time"
I think in the world of implicit tasks (no !), we can't have special if without else syntax.
It doesn't work anymore
Cause there is no way to tell if it is valid and a task or invalid and a pure function
At a minimum it is really strange/inconsistent syntax that would have different expected results base on purity
I see the problem, yeah
so this would work in Rust because ? is sugar for an early return
and therefore shortcuts to the end of the function
thinking about this more, I think we might need to make ? work the way it does in Rust in the Purity Inference design
consider this code that works today:
foo = \arg ->
if arg == 0 then
Stdout.line! "arg was zero!"
File.writeUtf8 path "it was zero"
else
Task.ok {}
File.writeUtf8! path2 "got this arg: $(arg)"
Stdout.line! "wrote to file"
Task.ok arg
the reason this works is that if is a statement - that is, we aren't using it as an expression - and we insert a Task.await in between statements automatically
in the Purity Inference world, the same would be true inside effectful functions: we'd insert an implicit "await the next effect" whenever there's a statement
however, there's a crucial difference: in the Purity Inference world, awaiting an effect does not involve error handling
in other words, Task.await handles both errors and effects, whereas those are separated in the Purity Inference design
so now let's take that same example and move it to the Purity Inference world, but with ? instead of !
foo = \arg ->
if arg == 0 then
Stdout.line? "arg was zero!"
File.writeUtf8 path "it was zero"
else
Ok {}
File.writeUtf8? path2 "got this arg: $(arg)"
Stdout.line? "wrote to file"
Ok arg
in the previous example, the if was evaluating to Task {}, which is what statements are supposed to do
in the Purity Inference world, statements should evaluate to {} instead of Task {}
but this if is not evaluating to {}, it's evaluating to Result {} _
and there's no automatic short-circuiting of that in between statements like there was with Task.await (because it did both effects and error handling at the same time)
I think this might reveal how Rust ended up with the ? design that they did: it doesn't really work the other way
so if we changed ? to work the way that it does in Rust, then that would mean that foo? is essentially syntax sugar for:
when foo is
Ok val -> val
Err err -> return Err err
and then an actual return keyword is needed (or at least the concept, regardless of whether it's user-facing, because an early return is exactly what ? would do) - which would be a relevant consideration for #ideas > "early returns" via formatter
an alternative design might be to introduce a rule like:
"any if statement that includes a ? effectively gets a ? wrapped around the whole thing"
that would be a more minor change than introducing a full concept of "early return," although it feels harder to explain
another option, which I don't love, is to make it opt-in with if?
I don't love that because you'd basically always want that whenever you had an if statement that used ? inside it, so adding it would essentially be a chore that could have been inferred
but the upside would be that it would make the control flow more explicit
I think there are interesting pros and cons to the idea of introducing an explicit return operator
obviously it's convenient in some cases
but it does have an innate tradeoff with control flow being less obvious; it's no longer just "nested expressions all the way down"
although to be fair, the ! and ? operators we already have today muddy those waters
having an explicit return operator would remove a class of beginner questions, which is nice
Whether we have a user-facing return keyword or not, I think this exactly how I would expect ? to work with the statement version of if
as in you'd expect it to do an early return, like it does in Rust?
Yeah, I know it’s different from what you’d expect in a expression if, but I think it makes sense for the statement version
Well, would it always be an early return from the the current function? What if it's in a value def?
like
bar = \arg ->
foo =
if arg == 0 then
Stdout.line? "arg was zero!"
File.writeUtf8 path "it was zero"
else
Ok {}
File.writeUtf8? path2 "got this arg: $(arg)"
Stdout.line? "wrote to file"
Ok arg
Result.withDefault "something went wrong in foo" foo
In this case, we don't want an Err in foo to return bar because bar doesn't return a Result at all
I feel like that should work, but I haven't thought about the desugaring yet
Richard Feldman said:
an alternative design might be to introduce a rule like:
"any
ifstatement that includes a?effectively gets a?wrapped around the whole thing"
I guess what I am describing is what you meant with this
I think this is how people expect ? to work anyway
And the automatic ? propagation I think acts like early return, right?
So maybe since ! is going away, we don't need to preserve the Result.try impl of ?
I think it shouldn't work exactly like early return, as in "return from the current function"
It's more like "return from the nearest def"
I'm not sure if an early return operator/keyword is a good idea, but at least this kind of needs to be an early return
Agus Zubiaga said:
Richard Feldman said:
an alternative design might be to introduce a rule like:
"any
ifstatement that includes a?effectively gets a?wrapped around the whole thing"I guess what I am describing is what you meant with this
should it work the same way for if expressions or just if statements?
answer =
if arg == 0 then
Stdout.line? "arg was zero!"
File.readUtf8 path
else
Ok ""
That already returns a Result, doesn't it? The ? desugaring can happen inside the if
yeah the question is whether (for consistency with if statements) answer should be a Result or if it should have an implicit ? too
meaning it would be a Str and would short-circuit
Ah, I see what you mean
I think it's more intuitive if only statements short-circuit
well, if it's implicit then you cannot catch it from the outside, right?
even if it's less consistent
yeah, I think so too
because what else could it possibly mean haha
it gives you more control if they don't
foo = \arg ->
answerResult =
if arg == 0 then
Stdout.line? "arg was zero!"
File.readUtf8 path
else
Ok ""
# I can recover from the Err here
when answerResult ...
# and if I just want it to propagate, all I have to do is
answer = answerResult?
...
That's pretty inconvenient IMO, but yes, it gives more control if you don't want an early return
No, I'm saying you could do both
If it’s a statement if (not assigned to a def), it’s essentially wrapped in a ?
If it’s an expression if (assigned to def/field or passed to function) then it remains a Result
(the if statement gets wrapped in a ? if and only if at least one of its branches contains a ?)
Right, because you might just run an effectful function that doesn’t return Result
Works for me! It allows us to use ? in blocks as "early returns to the top of blocks".
An additional requirement could be that a statement if must return {} after unwrapped
To prevent people from ignoring results
If they want to ignore it, they can do:
_ = ignoredThing
{}
yeah Rust does that too
which I think is correct - means you can't accidentally swallow errors
Yeah, errors or meaningful values from function calls
Statement if could be nice for dbg too:
foo = \x ->
if x > 10 then dbg x
…
Or multiple expect too
If it’s only one there’s no point because you can just put it in the condition
I'm not quite sure the agreement this landing on, but I definitely don't think ? should default to an early return. It should still be local enough to slow capturing the result to a variable.
Frankly, without ! it is kinda weird we have multiple statement bodies at all.
Like it is a strange dichotomy in the language that some functions make valid statements but others don't. Imagine I make a set of tasks with no errors:
main = \{} ->
task1 {}
task2 {}
if task4 ... then
task5 ...
else
{}
task3 ....
:point_up: working with impure functions but failing with pure functions feels really strange. Especially given there is no syntax difference between the two.
Brendan Hansknecht said:
I'm not quite sure the agreement this landing on, but I definitely don't think
?should default to an early return. It should still be local enough to slow capturing the result to a variable.
yeah that's kinda where we ended up
Ok, just making sure :smiley:
example:
foo = \arg ->
answerResult = # this is a Result
if arg == 0 then
Stdout.line? "arg was zero!"
File.readUtf8 path
else
Ok ""
# I can recover from the Err here
when answerResult ...
# and if I just want it to propagate, all I have to do is
answer = answerResult?
...
the change would be to statement-style if, like so:
foo = \arg ->
if arg == 0 then
Stdout.line? "arg was zero!"
File.writeUtf8 path
else
Ok {}
...
this would become equivalent to:
foo = \arg ->
{} = (
if arg == 0 then
Stdout.line? "arg was zero!"
File.writeUtf8 path
else
Ok {}
)?
...
in other words, using ? inside an if statement (not an expression) would add a ? around the entire if
(behind the scenes)
the answerResult stuff is confusing in this example
because there's really nothing else you could possibly want it to do :sweat_smile:
oops yeah, I just edited that out so it's not confusing haha
It's confusing if you assume you get Rust rules IMO. If you assume early return, it's not that. If you engage with it as Roc rules, it makes sense
I just meant that the statement-if example was confusing because it included answerResult which would not be available (he since removed it)
I don't think the syntax itself is confusing
Okay, good
:point_up: working with impure functions but failing with pure functions feels really strange. Especially given there is no syntax difference between the two.
Also, I think we can ignore this comment above now that I think about it more.
The rule is anything that returns {} can just be a statement.
If it returns a Result {} ... it can just be used alone with ?
Otherwise, a value needs to be explicitly ignored with _ = ...
That would work with both pure and impure functions and make sense.
I like that this design preserves the locality of how ? works today :smiley:
I know I'm a bit late and still piecing everything together, but will it be easy to predict what happens when reading the code?
Richard Feldman said:
in other words, using
?inside anifstatement (not an expression) would add a?around the entireif
How do you tell whether it's a statement if or an expression if? Does it require knowing the types of the branches to tell how the control flow will work?
What if the example was this instead? I removed a line. The branches are the same type (Result {} _) but now there's no ? inside. Does that change the behavior of the if at all?
foo = \arg ->
if arg == 0 then
File.writeUtf8 path
else
Ok {}
...
That does change things
Actually I'm pretty sure the first example was accidentally invalid
It should have been this for the example:
foo = \arg ->
if arg == 0 then
Stdout.line? "arg was zero!"
File.writeUtf8? path
else
Ok? {}
...
Cause all of the results need to have a ? to propagate. And they are required to propagate due to the ... containing extra code afterwards
Without the ... the final ?s in each branch could be dropped.
Last updated: Jun 16 2026 at 16:19 UTC