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
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
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 {}
Thanks @Kiryl Dziamura . I guess I'll open an issue on github then.
Anyway, Task
is going to become part of the roc std very soon and will be removed from platforms
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!
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.
We could swap from List.walk to List.walkBackwards
I opened https://github.com/roc-lang/basic-cli/issues/235
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.
Fixed
$ roc examples/hello-world.roc
🔨 Rebuilding platform...
1
2
3
https://github.com/roc-lang/roc/pull/6836/commits/aabe75ff87bd105e293eb1e7c6944f9fc6141ffd
It wasn't a big change :smiley:
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]
I guess we could use, List.prepend
to build the results up.
It looks like it collected tasks in correct order, but resolved them in LIFO
Switching to List.prepend we get this,
$ roc examples/hello-world.roc
🔨 Rebuilding platform...
1
2
3
Got nums: [1, 2, 3]
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
!tcefreP
Hmmm, I'm thinking the List.append is the correct solution even though it may be a little surprising at first
But, I'm definitely not confident about that
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
Well the list is built up one element at a time, so the first element in will be in the last position.
Yeah, I'd say the implementation looks kind of weird now but input vs output seems more logical in the last version
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.
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]
I might also add a List.withCapacity
in there too
How does this look?
Yeah I think this is better
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
I think I agree too.
maybe initListTask
instead of init
? If I see init
in some code without context that could be anything :p
I suspect this will be a lot faster by doing appends while building it up and then doing one reverse at the end
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
whereas appends are super cheap, and 1 reverse should be about the same cost as one of the prepends
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.
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.
That way the state accumulates and wraps the final task instead of repeatedly being placed inside the newest task.
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.
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.
That's great, thanks @Luke Boswell , I'll try this now. :+1:
Last updated: Jul 06 2025 at 12:14 UTC