I played around with the following code and couldn't understand why it didn't work as expected:
-- unexpected type annotation
ignoreResult : Task.Task ok err -> Task.Task (Result ok err) *
ignoreResult = \task ->
Task.result! task
Task.ok {}
-- desugared version
ignoreResultDesugared : Task.Task ok err -> Task.Task (Result ok err) *
ignoreResultDesugared = \task ->
Task.result task
-- what I expected
ignoreResultExpected : Task.Task ok err -> Task.Task {} *
ignoreResultExpected = \task ->
Task.await (Task.result task) \_ ->
Task.ok {}
currently, desugaring works this way:
-- initial code
ignoreResult = \task ->
Task.result! task
Task.ok {}
-- 1. add record destructuring in front of the statement (why not underscore?)
ignoreResult = \task ->
{} = Task.result! task
Task.ok {}
-- 2. unwrap TaskAwaitBang to a Task.await call preserving record destructuring
ignoreResult = \task ->
Task.await (Task.result task) \{} ->
Task.ok {}
-- 3. drop destructuring because input and output are the same. it's incorrect
ignoreResult = \task ->
Task.result task
what I want to do:
_ instead of {} in step 23Just want to confirm that it's a bug and the fix logic is correct
Yeah. That sounds like a bug and correct fix
going to create a pr soon then
Oh, one quick thought
result! returns data, so maybe we intentionally want to force users to write:
_ = Task.result! task
Like be explicit they meant to throw away the data.
I could see the value in that (of course it would need a custom error to avoid confusion)
I believe there was a discussion a long time ago and as a result, statements were introduced. otherwise we'd write _ = everywhere.
Or you mean, allow statements only for Task {} * case?
I would agree with this.
In Rust, we have the #[must_use] directive, which makes sure that users don't perform fallible tasks and forget to handle a Result
Without such a directive here, if we don't require users to at least put an underscore, it becomes very easy to avoid handling task outcomes
I like the idea. not sure how to implement it tho. it's a type based unused variable
The simple thing is what I think we already do: allow skipping the assignment if the {} unit type is returned, otherwise require an assignment
can you give an example?
The fix for the problem described in the head message: https://github.com/roc-lang/roc/issues/6868
I think an example is this:
x = Stdin.line!
{} = Stdout.line! "some output"
We definitely want to allow:
x = Stdin.line!
Stdout.line! "some output"
We actually might not want to allow:
Stdin.line!
Stdout.line! "some output"
With this final code, we may want to force the user to be explicit that they are throwing away the result of Stdin.line!. So We may want to generate an error that say: Stdin.line! returns a Str, but you are not using it. If this is intentional, please write "_ = Stdin.line!"
As such, the required form would be:
_ = Stdin.line!
Stdout.line! "some output"
Basically, the question is: Do we want warnings for when the result of a Task! is not used and is not {}?
I could see people voting either way. I think the explicit _ = is a lot clearer on intention, but is extra verbosity. It also, likely will be a bit harder to program into the compiler, but I think it is likely the better solution so that a user doesn't accidentally miss a returned value from a task.
yeah I think statements should only work for Task {} _
Yeah, I agree with all points. I meant, an example of what @Sam Mohr was talking about.
I imagined suffixed stmt desugaring to work this way, in particular the first step:
ignoreResult = \task ->
Task.result! task
Task.ok {}
-- 1. add a unique var in front of the statement along with a type annotation
ignoreResult = \task ->
#!a0 : {}
#!a0 = Task.result! task
Task.ok {}
and then if not a unit type returned - ask user to have an explicit pattern. otherwise - ignore the unused variable (assuming it's a special kind of var)
However, no need to add the annotation if there’s a special kind of pattern. I’ll play with the concept. I don’t know how errors/warnings of the compiler are collected, and didn’t look into type system much yet, so it will be a good introduction I think
Kiryl Dziamura said:
Yeah, I agree with all points. I meant, an example of what Sam Mohr was talking about.
I might be communicating poorly, I'm just saying what Richard said. If the Task returns {}, we know nothing valuable was returned. Otherwise, if the dev is forced to assign the value to a variable, we get what I'm talking about for free
In that if they assign a variable that's not prefixed with an underscore, they'll get a warning if the value isn't used. So they need to explicitly ignore the value by prefixing with an underscore to ignore it, which is good enough as a #[must_use] in my eyes
Then we’re on the same page! :smile:
Awesome!
I think there may have been some confusion here. At least I'm a little confused about the PR. -- I'll try and explain 1 sec
I thought were desugaring suffixed statements into {} = Task.await ... because that restricts their use to only tasks that are Task {} _.
For example the following example is not the behaviour that we want
main =
Stdin.input!
Stdout.print "just ingore the input"
# the PR would desguar this into
main =
Task.await (Stdin.input) \_ ->
Stdout.print "just ingore the input"
While this should be permitted because the user is intentionally throwing the input away
main =
_ = Stdin.input!
Stdout.print "just ingore the input"
# this should be ok, and is the current bevahiour (ASFAIK)
main =
Task.await (Stdin.input) \_ ->
Stdout.print "just ingore the input"
And if they just make it a statement and don't explicitly ignore, like below, then this should throw a type error.
main =
Stdin.input!
Stdout.print "just ingore the input"
# this will throw a type error
main =
Task.await (Stdin.input) \{} ->
Stdout.print "just ingore the input"
── TYPE MISMATCH in main.roc ───────────────────────────────────────────────────
This 2nd argument to this function has an unexpected type:
10│ Stdin.input!
The argument is an anonymous function of type:
{}a -> Task {} […]
But this function needs its 2nd argument to be:
Str -> Task {} […]
────────────────────────────────────────────────────────────────────────────────
@Luke Boswell are you saying this is how you thought we agreed it should work, or how you personally think it should work?
I think users should be able to use ! to await any Task, and eliding the {} = means when the task returns {}, we can throw that away because it's useless
Richard Feldman said:
yeah I think statements should only work for
Task {} _
This is basically what I'm thinking of, and why I don't think we should have statements for any task.
Brendan Hansknecht said:
result!returns data, so maybe we intentionally want to force users to write:_ = Task.result! task
And also this. If users want to throw away the data they can add a _ = assignment and be explicit.
Sam Mohr said:
Luke Boswell are you saying this is how you thought we agreed it should work, or how you personally think it should work?
I'm saying I think there has been some confusion. Based on my read of the above discussion, and the behaviour I can see in the PR, I don't think it matches.
I'm seeking clarification -- as much as pointing out how I also understand the design was intended to work.
Okay, when we say "statement", we mean something like
Stdout.line! "Hello world"
Where we call a task Task {} _, but don't assign anything. Whereas an "assignment" means
data = Http.get! "api.com"
Okay, given that much
Yeah, it's a very specific distinction based on the implementation for the Ast::Exprtype
We only have statements for dbg expect if then else when
Luke Boswell said:
Richard Feldman said:
yeah I think statements should only work for
Task {} _This is basically what I'm thinking of, and why I don't think we should have statements for any task.
You would rather not have "statements", aka users should have to write
{} = Stdout.line! "Hello world"
Is that the case?
This is discussed in the Chaining Syntax Design Proposal
See the section Task statements
I'll read through that again. In the mean time, I think I personally am okay with eliding the left hand side of this assignment for a few reasons
{} = is unambiguous if it only happens with unit value Tasksyeah so maybe a summary would be:
{} = someTask! ... because you should always be able to omit that {} =Task return types, this shouldn't be the case, and you should have to write _ = someTask! (or some more specific pattern than _)@Luke Boswell thanks for clarification!
I updated the pr: https://github.com/roc-lang/roc/pull/6868
the actual fix is to just remove the third step I mentioned in my original message:
ignoreResult = \task ->
Task.await (Task.result task) \{} ->
Task.ok {}
-- 3. drop destructuring because input and output are the same
ignoreResult = \task ->
Task.result task
In this step, we completely remove user's return Task.ok {}, assuming the task returns {} which is not correct because we don't know it's type during desugaring
What about records? Say you do something like Http.get which returns a response record like { status : U16, body : Str } (simplified of course). With await syntax you used to be able to do
Task.await (Http.get "example.com") \{} ->
# do something without response
The record fields would just be ignored because the anonymous function takes an open record. I don't know how this would work with bang syntax, but it shouldn't work as a statement for such calls. However just desugaring a statement to an empty record would allow it to work. I don't know the way around it though because you would need type information
Yeah, I think we should generate a special pattern as I mentioned earlier.
Or, even better, we can have a named underscore with type annotation
_#!a0 : {}
_#!a0 = Task.await! task
Is there a way to annotate closure without moving it into a separate variable?
I imagine smth like that
ignoreResult = \task ->
Task.result! task
Task.ok {}
transforms to
ignoreResult = \task ->
Task.await (Task.result task) \a ->
_a : {}
_a = a
Task.ok {}
it works and requires strictly unit type. however in this form it produces this warning:
This destructure assignment doesn't introduce any new variables:
14│ _a = a
^^
If you don't need to use the value on the right-hand-side of this
assignment, consider removing the assignment. Since Roc is purely
functional, assignments that don't introduce variables cannot affect a
program's behavior!
which is expectable but I'm not sure how it would look like for syntax sugar. we should ignore this warning there
Ha! I think it can be this:
ignoreResult = \task ->
Task.await task \a0 ->
a1 : {}
a1 = a0
Task.ok a1
Found another bug. Type annotation is ignored in this example:
f = \task ->
a : U32
a = task!
Task.ok a
The fix from the prev message should cover this case as well
On the other hand, it won't work with multiple statements:
f =
a!
b!
c!
f =
Task.await a \#!a0 ->
#!a1 : {}
#!a1 = #!a0 -- <---- need to drop this variable without warnings somehow
Task.await b \#!a2 -> ...
I'll extend Underscore(&str) to Underscore(&str, AddedBy) where AddedBy is enum with options User, TaskAwaitBang (and ResultTryQuestion in the future) so we can both fix the problem and generate better error message going forward
Sorry for spamming. I realized that typed closures are better for now. This way no need to introduce changes to the Underscore pattern. At least for now
f = \x, y, z ->
x!
y!
z!
Task.ok {}
f = \x, y, z ->
a2 : {} -> _
a2 = \_ -> Task.ok {}
a1 : {} -> _
a1 = \_ -> Task.await z a2
a0 : {} -> _
a0 = \_ -> Task.await y a1
Task.await x a0
Last updated: Nov 28 2025 at 12:16 UTC