I'm currently looking into an issue with await bang desugaring in defs and want to clarify my understanding:
If suffixed expression is part of another expression - it should be desugared like this:
main =
a b!
main =
Task.await b \#!a0 ->
a #!a0
But if it's a top level bang - it should return an empty record:
main =
a!
main =
Task.await a \{} ->
Task.ok {}
Meaning, the following two snippets have different type signatures (let's assume a
is Task U8
):
main : Task {}
main =
a!
main : Task U8
main =
a
Currently it works inconsistently (can't provide an exhaustive list of problems unfortunately, but here's the ticket I'm working on). I just want to clarify that my intuition is correct so I can fix this logic in parser and desugaring.
But if it's a top level bang - it should return an empty record:
I think in this case it won't be unwrapped, but just ignored.
In the implementation of apply_task_await
there are two functions which should detect this case is_matching_empty_record
and is_matching_intermediate_answer
and just make this a NOOP
So for these two examples you have shown, the first;
main =
Task.await b \#!a0 ->
a #!a0
Should hit the is_matching_intermediate_answer
and just return the b
expr.
For the second;
main =
Task.await a \{} ->
Task.ok {}
This should hit the is_matching_empty_record
and just return the a
expr.
Luke Boswell said:
For the second;
main = Task.await a \{} -> Task.ok {}
This should hit the
is_matching_empty_record
and just return thea
expr.
But it’s not the same. If I wrote it manually, I wouldn’t expect Task.ok {}
would become a
The first
Could you please elaborate?
Another way to think about it. I think it should work this way:
Invalid
f : Bool, Task a, Task b -> Task {}
f = \x, a, b ->
if x then
a
else
b
Valid
f : Bool, Task a, Task b -> Task {}
f = \x, a, b ->
if x then
a!
else
b!
Ah, this is a question if bang should throw away the result
I think this is a case where the exact semantics of bang are a bit complex
And arguably inconsistent
I think the expected behaviour above is a type mismatch in both cases
These would be correct though
I think this is the expected behaviour:
f : Bool, Task a, Task a -> Task a
f = \x, a0, a1 ->
if x then
a0!
else
a1!
g : Task a -> Task a
g = \a ->
a!
h : Bool, Task a, Task a, Task b -> Task b
h = \x, a0, a1, b ->
if x then
a0!
else
a1!
b!
I think the correct mental model is that !
always tries to return a result. It does not end with a Task.ok {}
That said, a following bang will throw away the result of the previous bang.
My take is that bang is expression if it’s part of another expression. If it’s top-level (in place of assignment) - it’s a destructuring statement. I.e:
main =
a!
b!
c!
main =
{} = a!
{} = b!
{} = c!
{}
Desugared with the redundant await (which wouldn't actually be emitted, but I think make the desugaring clearer) these would be:
f : Bool, Task a, Task a -> Task a
f = \x, a0, a1 ->
Task. await (
if x then
a0
else
a1
) \a -> Task.ok a
g : Task a -> Task a
g = \a ->
Task. await a \a2 -> Task.ok a2
h : Bool, Task a, Task a, Task b -> Task b
h = \x, a0, a1, b ->
Task. await (
if x then
a0
else
a1
) \a ->
# It can be imgained that there is an igorned `_ = Task.ok a` here.
Task.await b \b2 -> Task.ok b2
My take is that bang is expression if it’s part of another expression
This can be debated in ideas, but the current design is to explicitly allow a trailing !
which essentially doesn't actually desugar.
Should create the semantic I show in the desugaring directly above.
https://github.com/roc-lang/roc/issues/6851
So this fix was correct then?
I don't know that code enough to tell for sure.
Fundamentally, with the current design, ending a function with a!
or with a
should result in the exact same final result.
So in theory, to return an empty record, I’d do it explicitly? (I’m from mobile rn, can’t check)
main =
a!
b!
c!
{}
I think supporting the final or trailing expression so it doesnt get unwrapped is why we have EmptyDefsFinal.
It currently would need to be:
main =
a!
b!
c!
Task.ok {}
We talked about removing the need for the Task.ok
, don't recall which thread, but I don't think anything was decided around that.
Assuming c!
returns a Task c
where c
is not {}
From what I saw in the code, EmptyDefsFinal
unwraps into Task.ok {}
and is used only if the last statement in defs is the bang. Thus why I started this thread - I removed EmptyDefsFinal
completely and it works as @Brendan Hansknecht outlined. But then I looked at the code again and understood that it was probably a different intention.
Explicit {}
return is reasonable because a!
desugared to a
is intuitive. As I said, that’s how I expected it to work initially. However, the other way makes sense as well, just another mental model. I’ll describe my thoughts in #ideas later. The current thread is for fixing the bug without introducing breaking changes.
Last updated: Jul 06 2025 at 12:14 UTC