Stream: beginners

Topic: Task.sequence reverses the list order?


view this post on Zulip Aurélien Geron (Jul 29 2024 at 08:58):

It looks like Task.sequence reverses the list order, is this expected? For example:

app [main] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.12.0/Lb8EgiejTUzbggO2HVVuPJFkwvvsfW6LojkLR20kTVE.tar.br" }

import cli.Task
import cli.Stdout

run : U8 -> Task.Task _ _
run = \n ->
    n |> Num.toStr |> Stdout.line!

main =
    _ = [1,2,3] |> List.map run |> Task.sequence!
    Task.ok {}

When I run this code I get this:

$ roc test.roc
3
2
1

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

It seems to be expected, although not documented :thinking:
https://github.com/roc-lang/basic-cli/blob/79bb5e88ea3cdb2973a43f3c7b0847936f204c69/examples/task-list.roc#L15

There's also forEach method: https://www.roc-lang.org/packages/basic-cli/0.12.0/Task#forEach

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

This works as expected. But of course the identity function in the callback is not what we want here. I think it's a bug in
Task.sequence indeed as Task.sequence list should work almost the same as Task.forEach list \x -> x

app [main] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.12.0/Lb8EgiejTUzbggO2HVVuPJFkwvvsfW6LojkLR20kTVE.tar.br" }

import cli.Task
import cli.Stdout

run : U8 -> Task.Task _ _
run = \n ->
    n |> Num.toStr |> Stdout.line!

main =
    _ = [1, 2, 3] |> List.map run |> Task.forEach! \x -> x
    Task.ok {}

view this post on Zulip Aurélien Geron (Jul 29 2024 at 09:17):

Thanks @Kiryl Dziamura . I guess I'll open an issue on github then.

view this post on Zulip Kiryl Dziamura (Jul 29 2024 at 09:17):

Anyway, Task is going to become part of the roc std very soon and will be removed from platforms

view this post on Zulip Kiryl Dziamura (Jul 29 2024 at 09:19):

If you're interested, here's the pr: https://github.com/roc-lang/roc/pull/6836
The bug will probably be there as well, so you can link your issue there for visibility.

And thank you for the report!

view this post on Zulip Luke Boswell (Jul 29 2024 at 09:40):

I don't think it's deliberate, I just implemented it the first way I thought of and didn't consider this or notice.

It does seem a little unexpected to me.

I think we should look at this. It might be easy to change, just taking from the other end of the list.

view this post on Zulip Luke Boswell (Jul 29 2024 at 09:41):

We could swap from List.walk to List.walkBackwards

view this post on Zulip Aurélien Geron (Jul 29 2024 at 09:45):

I opened https://github.com/roc-lang/basic-cli/issues/235

view this post on Zulip Luke Boswell (Jul 29 2024 at 09:46):

I might see if I can roll a fix into the builtin-task branch while I have it open, and it's pretty minor change.

view this post on Zulip Luke Boswell (Jul 29 2024 at 09:50):

Fixed

$ roc examples/hello-world.roc
🔨 Rebuilding platform...
1
2
3

view this post on Zulip Luke Boswell (Jul 29 2024 at 09:53):

https://github.com/roc-lang/roc/pull/6836/commits/aabe75ff87bd105e293eb1e7c6944f9fc6141ffd

view this post on Zulip Luke Boswell (Jul 29 2024 at 09:53):

It wasn't a big change :smiley:

view this post on Zulip Luke Boswell (Jul 29 2024 at 09:56):

The only thing is it reverses the order of the results... so if we had the following

app [main] { pf: platform "../platform/main.roc" }

import pf.Stdout

run : U8 -> Task U8 _
run = \n ->
    n |> Num.toStr |> Stdout.line!

    Task.ok n

main =
    nums = [1,2,3] |> List.map run |> Task.seq!

    Stdout.line! "Got nums: $(Inspect.toStr nums)"

    Task.ok {}

We get the following

$ roc examples/hello-world.roc
🔨 Rebuilding platform...
1
2
3
Got nums: [3, 2, 1]

view this post on Zulip Luke Boswell (Jul 29 2024 at 09:58):

I guess we could use, List.prepend to build the results up.

view this post on Zulip Kiryl Dziamura (Jul 29 2024 at 09:58):

It looks like it collected tasks in correct order, but resolved them in LIFO

view this post on Zulip Luke Boswell (Jul 29 2024 at 09:59):

Switching to List.prepend we get this,

$ roc examples/hello-world.roc
🔨 Rebuilding platform...
1
2
3
Got nums: [1, 2, 3]

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

So this impl

seq : List (Task ok err) -> Task (List ok) err
seq = \tasks ->
    List.walkBackwards tasks (ok []) \state, task ->
        value <- task |> await

        state |> map \values -> List.prepend values value

view this post on Zulip Aurélien Geron (Jul 29 2024 at 10:03):

!tcefreP

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

Hmmm, I'm thinking the List.append is the correct solution even though it may be a little surprising at first

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

But, I'm definitely not confident about that

view this post on Zulip Anton (Jul 29 2024 at 10:12):

Hmmm, I'm thinking the List.append is the correct solution even though it may be a little surprising at first

Can you elaborate? Having it reversed seems weird to me

view this post on Zulip Luke Boswell (Jul 29 2024 at 10:13):

Well the list is built up one element at a time, so the first element in will be in the last position.

view this post on Zulip Anton (Jul 29 2024 at 10:23):

Yeah, I'd say the implementation looks kind of weird now but input vs output seems more logical in the last version

view this post on Zulip Luke Boswell (Jul 29 2024 at 10:24):

I think there might be two different ways of thinking about things here. This is a bit of a wild generalisation...

One is from a more mathematical/logical/functional perspective, the other is from a more concrete/imperative/procedural perspective.

I think having the sequence return the list return the results in the same order is closer to the former, as this would be the order they are executed.

While having them returned reversed is closer to the latter, where you might have a mental model of placing items in on a stack, placing them into a queue.

view this post on Zulip Luke Boswell (Jul 29 2024 at 10:27):

implementation looks kind of weird now

How does this look?

seq : List (Task ok err) -> Task (List ok) err
seq = \tasks ->
    List.walkBackwards tasks (ok []) \state, task ->
        await task \value ->
            map state \values -> List.prepend values value

This gives us

1
2
3
Got nums: [1, 2, 3]

view this post on Zulip Luke Boswell (Jul 29 2024 at 10:27):

I might also add a List.withCapacity in there too

view this post on Zulip Anton (Jul 29 2024 at 10:29):

How does this look?

Yeah I think this is better

view this post on Zulip Luke Boswell (Jul 29 2024 at 10:30):

With capacity

seq : List (Task ok err) -> Task (List ok) err
seq = \tasks ->

    init = ok (List.withCapacity (List.len tasks))

    List.walkBackwards tasks init \state, task ->
        await task \value ->
            map state \values -> List.prepend values value

view this post on Zulip Luke Boswell (Jul 29 2024 at 10:30):

I think I agree too.

view this post on Zulip Anton (Jul 29 2024 at 10:31):

maybe initListTask instead of init? If I see init in some code without context that could be anything :p

view this post on Zulip Richard Feldman (Jul 29 2024 at 12:37):

I suspect this will be a lot faster by doing appends while building it up and then doing one reverse at the end

view this post on Zulip Richard Feldman (Jul 29 2024 at 12:39):

because doing a bunch of prepends in a row is a pathological case for a list, since each one has to copy and shift the entire previous list

view this post on Zulip Richard Feldman (Jul 29 2024 at 12:39):

whereas appends are super cheap, and 1 reverse should be about the same cost as one of the prepends

view this post on Zulip Oskar Hahn (Jul 30 2024 at 06:47):

I don't understand the issue. I can see, that the current implementation returns the values in the reverse order. But when looking at the current code, I can not understand why. This is the current code:

sequence : List (Task ok err) -> Task (List ok) err
sequence = \tasks ->
    List.walk tasks (InternalTask.ok []) \state, task ->
        value <- task |> await

        state |> map \values -> List.append values value

It walks the list of task from start to end. On each step, it places the result at the end of the list. This looks correct to me. How is it possible, that the last task in the list gets the first element in the result list? Is there something special about tasks I don't understand?

The two issues I see are the missing capacity and that the function does not stop running the tasks on the first error. I would have guessed, that something like List.walkUntil or List.walkTry would be used.

view this post on Zulip Brendan Hansknecht (Jul 30 2024 at 07:10):

It is building up a giant chain of closures. The outer most closure will be the last task in the list.

The inner body should be:

values = state!
value = task!
Task.ok (List.append values value)

Or something along those lines to get the ordering correctly.

view this post on Zulip Brendan Hansknecht (Jul 30 2024 at 07:12):

That way the state accumulates and wraps the final task instead of repeatedly being placed inside the newest task.

view this post on Zulip Brendan Hansknecht (Jul 30 2024 at 07:24):

That said, I would probably write it as

sequence = \taskList ->
    Task.loop (taskList, List.withCapacity (List.len taskList)) \(tasks, values) ->
        when tasks is
            [task, .. as rest] ->
                value = task!
                Step (rest, List.append values value)
                |> Task.ok
            [] ->
                Done values
                |> Task.ok

Task.loop is special and less likely to break with weird edge cases related to lambdaset nesting and such. So I think it is the best option here.

view this post on Zulip Luke Boswell (Jul 30 2024 at 07:45):

Well I think we found our issue with https://roc.zulipchat.com/#narrow/stream/231634-beginners/topic/Compiler.20seems.20extremely.20slow.20on.20some.20code/near/454811693

I just updated to your solution above and ran that using the latest Task as Builtin branch for both roc and basic-cli and it compiles and runs fine.

view this post on Zulip Aurélien Geron (Jul 30 2024 at 07:57):

That's great, thanks @Luke Boswell , I'll try this now. :+1:


Last updated: Jul 06 2025 at 12:14 UTC