Stream: contributing

Topic: TaskAwaitBang statement desugaring


view this post on Zulip Kiryl Dziamura (Jul 04 2024 at 16:16):

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:

  1. use _ instead of {} in step 2
  2. remove step 3

Just want to confirm that it's a bug and the fix logic is correct

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 16:25):

Yeah. That sounds like a bug and correct fix

view this post on Zulip Kiryl Dziamura (Jul 04 2024 at 16:26):

going to create a pr soon then

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 16:40):

Oh, one quick thought

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 16:41):

result! returns data, so maybe we intentionally want to force users to write:

_ = Task.result! task

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 16:41):

Like be explicit they meant to throw away the data.

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 16:42):

I could see the value in that (of course it would need a custom error to avoid confusion)

view this post on Zulip Kiryl Dziamura (Jul 04 2024 at 16:48):

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?

view this post on Zulip Sam Mohr (Jul 04 2024 at 16:48):

I would agree with this.

view this post on Zulip Sam Mohr (Jul 04 2024 at 16:49):

In Rust, we have the #[must_use] directive, which makes sure that users don't perform fallible tasks and forget to handle a Result

view this post on Zulip Sam Mohr (Jul 04 2024 at 16:50):

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

view this post on Zulip Kiryl Dziamura (Jul 04 2024 at 16:52):

I like the idea. not sure how to implement it tho. it's a type based unused variable

view this post on Zulip Sam Mohr (Jul 04 2024 at 16:56):

The simple thing is what I think we already do: allow skipping the assignment if the {} unit type is returned, otherwise require an assignment

view this post on Zulip Kiryl Dziamura (Jul 04 2024 at 16:59):

can you give an example?

view this post on Zulip Kiryl Dziamura (Jul 04 2024 at 18:38):

The fix for the problem described in the head message: https://github.com/roc-lang/roc/issues/6868

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 19:05):

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 {}?

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 19:07):

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.

view this post on Zulip Richard Feldman (Jul 04 2024 at 19:35):

yeah I think statements should only work for Task {} _

view this post on Zulip Kiryl Dziamura (Jul 04 2024 at 19:35):

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 {}

view this post on Zulip Kiryl Dziamura (Jul 04 2024 at 19:37):

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)

view this post on Zulip Kiryl Dziamura (Jul 04 2024 at 20:07):

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

view this post on Zulip Sam Mohr (Jul 04 2024 at 20:30):

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

view this post on Zulip Sam Mohr (Jul 04 2024 at 20:32):

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

view this post on Zulip Kiryl Dziamura (Jul 04 2024 at 20:39):

Then we’re on the same page! :smile:

view this post on Zulip Sam Mohr (Jul 04 2024 at 20:39):

Awesome!

view this post on Zulip Luke Boswell (Jul 04 2024 at 23:58):

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

view this post on Zulip Luke Boswell (Jul 05 2024 at 00:05):

I thought were desugaring suffixed statements into {} = Task.await ... because that restricts their use to only tasks that are Task {} _.

view this post on Zulip Luke Boswell (Jul 05 2024 at 00:08):

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"

view this post on Zulip Luke Boswell (Jul 05 2024 at 00:09):

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"

view this post on Zulip Luke Boswell (Jul 05 2024 at 00:13):

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 {} […]

────────────────────────────────────────────────────────────────────────────────

view this post on Zulip Sam Mohr (Jul 05 2024 at 00:21):

@Luke Boswell are you saying this is how you thought we agreed it should work, or how you personally think it should work?

view this post on Zulip Sam Mohr (Jul 05 2024 at 00:22):

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

view this post on Zulip Luke Boswell (Jul 05 2024 at 00:29):

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.

view this post on Zulip Luke Boswell (Jul 05 2024 at 00:30):

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.

view this post on Zulip Luke Boswell (Jul 05 2024 at 00:33):

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.

view this post on Zulip Sam Mohr (Jul 05 2024 at 00:56):

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"

view this post on Zulip Sam Mohr (Jul 05 2024 at 00:57):

Okay, given that much

view this post on Zulip Luke Boswell (Jul 05 2024 at 00:58):

Yeah, it's a very specific distinction based on the implementation for the Ast::Exprtype

view this post on Zulip Luke Boswell (Jul 05 2024 at 00:59):

We only have statements for dbg expect if then else when

view this post on Zulip Sam Mohr (Jul 05 2024 at 00:59):

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"

view this post on Zulip Sam Mohr (Jul 05 2024 at 00:59):

Is that the case?

view this post on Zulip Luke Boswell (Jul 05 2024 at 01:01):

This is discussed in the Chaining Syntax Design Proposal

view this post on Zulip Luke Boswell (Jul 05 2024 at 01:01):

See the section Task statements

view this post on Zulip Sam Mohr (Jul 05 2024 at 01:07):

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

view this post on Zulip Richard Feldman (Jul 05 2024 at 01:07):

yeah so maybe a summary would be:

view this post on Zulip Kiryl Dziamura (Jul 05 2024 at 07:16):

@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

view this post on Zulip Kilian Vounckx (Jul 05 2024 at 07:33):

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

view this post on Zulip Kiryl Dziamura (Jul 05 2024 at 08:23):

Yeah, I think we should generate a special pattern as I mentioned earlier.

view this post on Zulip Kiryl Dziamura (Jul 05 2024 at 08:59):

Or, even better, we can have a named underscore with type annotation

view this post on Zulip Kiryl Dziamura (Jul 05 2024 at 09:06):

_#!a0 : {}
_#!a0 = Task.await! task

view this post on Zulip Kiryl Dziamura (Jul 05 2024 at 09:07):

Is there a way to annotate closure without moving it into a separate variable?

view this post on Zulip Kiryl Dziamura (Jul 05 2024 at 09:51):

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

view this post on Zulip Kiryl Dziamura (Jul 05 2024 at 10:39):

Ha! I think it can be this:

ignoreResult = \task ->
    Task.await task \a0 ->
        a1 : {}
        a1 = a0
        Task.ok a1

view this post on Zulip Kiryl Dziamura (Jul 05 2024 at 11:04):

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

view this post on Zulip Kiryl Dziamura (Jul 05 2024 at 11:56):

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

view this post on Zulip Kiryl Dziamura (Jul 05 2024 at 15:15):

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