What's the difference between Task {} {}
, Task {} []
and Task {} *
? For the latter two, what would be the rust-equivalent type?
Unit Type
Empty List
Anything -- in the return position there is nothing that can anything therefore this Task cannot fail.
I think how the function can then used is where the difference is most significant. I'll try and make an example
Is there any difference between Unit vs Anything? For anything, would the Rust type still be RocResult<(), ()>
?
For example ... using []
this is concrete and won't unify with the [Exit _ _] type.
app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br" }
main : Task {} [Exit I32 Str]
main =
foo!
Task.ok {}
foo : Task {} []
foo = Task.ok {}
── TYPE MISMATCH in main.roc ───────────────────────────────────────────────────
Something is off with the body of the main definition:
3│ main : Task {} [Exit I32 Str]
4│ main =
5│
6│ foo!
^^^^
This await call produces:
Task {} []
But the type annotation on main says it should be:
Task {} [Exit I32 Str]
To fix this we would need to return an open tag union like []_
.
So foo : Task {} []_
works
Jared Cone said:
Is there any difference between Unit vs Anything? For anything, would the Rust type still be
RocResult<(), ()>
?
You're specifically thinking about the interface between roc and the platform host here... This is currently generated as a RocResult and the *
is represented in rust using ()
Actually... you can't (shouldn't have) a *
being passed to the host
That will be a bug because it's size isn't fixed
You want a concrete fixed sized type for things you pass across between roc and the host
Alrighty. Ignoring host interface then, what would be the functional difference in a function that returns a Task {} {}
vs. a Task {} *
? I was using Task {} {}
to indicate a task that can't fail, but then I saw this in the tutorial:
Stdout.line has the type Task {} * because it doesn't produce any values when it finishes (hence the {}) and there aren't any errors that can happen when it runs (hence the *).
I suppose Task {} {}
means a task can fail but there's nothing useful in the error, but how does Task {} *
affect callers?
It's about how the error type unifies with other things, this affects where and how you use the function.
Task {} {}
: can succeed or fail but has no other information in either case.
Task {} []
: empty tag for the failure case. This means it can not fail. Will unify with any other tag based errors (e.g. Task {} [SomeErr]
)
Task {} *
: unconstrained type variable for the error case. This means it cannot fail. Can unify with anything.
Task {} {}
and RocResult<(), ()>
are equivalent. The other 2 are not actually safe to pass over the host boundary. Their exact implementation will change based on unification.
Would there be any reason to use Task {} []
instead of Task {} *
? Like, any reason a designer would want their task to only unify with other tasks that have tag-based errors?
Jared Cone said:
Would there be any reason to use
Task {} []
instead ofTask {} *
? Like, any reason a designer would want their task to only unify with other tasks that have tag-based errors?
I can't think of any reason why you would want to do that. I've used []
in the past because I didn't know the difference.
Only reason I can think of is to clarify that you expect a tag union for the error type. Like if all tasks use tag unions for errors and then a few have no error cases, I could see an argument for using []
over *
. Especially given I think a number of people find an empty tag easier to understand than *
.
Last updated: Jul 06 2025 at 12:14 UTC