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 23
Just 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::Expr
type
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: Jul 06 2025 at 12:14 UTC