hi is there any equivalent of c++ template in Roc
I am not saying it should be but just asking
specifically is there a way to do this :
template <class A>
f(A a )
{
// do something with a in generic way
}
No, there is no template system.
btw. templates are the reason among many others why I consider C++ piece of garbage, so lack of this ability although annoying at times may prove to be correct design choice, but indeed sometimes I am lacking something : )
But you can have generic types. For example List a
means a list of any type.
how to use that in function definition
Can you say more about what you want to do?
let say I want to create generic function to add two numbers how to do that ?
add : Num a, Num a -> Num a
add = \a, b -> a+b
You can only add numbers so it can't work on any type
will it work if I pass Float number to it ?
Yes
Num a stands for something or it is just tag as any in reality ?
I mean what to do for below function to be equivalent
add : Numm a, Numm a -> Numm a
add = \a, b -> a+b
Doesn't work. Numbers are special.
and I can't force Numm to act as Num ?
No
ok
It is built in
what is the role fo this "a" added after Num isn't Num just enough if it is build in ?
We started with the most complicated example unfortunately!
The a
is a type variable. It's similar to the typename in the C++ template
but Num specifies some restriction on this a ??
Yes Roc has builtin number types and all of them are specializations of the generic typeNum a
.
Numbers are the most complicated, let's talk about lists first for a minute
So List a
means a list of anything
List User
is a list of some type called User
List U8
is a list of unsigned 8 bit integers
So List a
is a generic type and List U8
is a specialized or concrete type.
That is how generic types work
can I put List b or "a" is keyword also ?
Yes
List b
or List whatever
But you can't change the List
and you can't change the Num
For numbers, U8
is a type alias for some Num a
where I can't remember what the a
is
Btw different topic ,I have this problem something is returning
-> Task {} *
but I somehow was not able to return
Task.err "something"
It's time for bed where I live so maybe someone else can answer that one, and I'd suggest making a new topic
ok thx anyway
If a function is returning Task {} *
, the *
means the error part of the Task
is empty. With -> Task {} *
you could only return Task.ok {}
or call a function like Stdout.line "done"
, which also returns Task {} *
.
If you want to return Task.err "something"
the signature would need to be -> Task <whatever> Str
. <whatever>
depends on what you want the function to return on success.
thx for explanation but what * means ? I was thinking it means anything just "*" coincides with anything in my mind : )
It used to confuse me too :)
We have a brief section on it in the tutorial.
The *
means compatible with any type.
For List.len
we have the signature len : List * -> Nat
because we can pass it a list of Str
, U64
, ... . To calculate the number of elements in the list, it does not actually matter what an element is.
When you return something with a wildcard type, it may not be immediately clear what that means. Say we have wildcardList : List *
, what could we fill in for wildcardList = ...
? The only value that can represent a list of any type is the empty list. So the only option is wildcardList = []
.
With Task
it's very similar. If we have the type Task Str *
, we have Str
for the success value and *
for the error value. Similar to List *
, the only error type that could satisfy *
is the empty error.
What’s the reason for using Task a * rather than Task a []?
Excellent question! I don't know, @Richard Feldman?
There's still some details about Roc's type system that I'm hazy on, but wouldn't the difference be that you can't use Task a *
when chaining Tasks with a tagged union error type?
Task.await
has a type of Task a b, (a -> Task c b) -> Task c b
, and if I use Task a []
for Task a b
and Task c [A, B, C]
for Task c b
, the type system can unify []
and[A, B, C]
. But if the error type were say, Task a {}
then it couldn't unify those types. Since Task a *
my be Task a {}
that means we can't necessarily unify those types and thus we would get a type error.
Give return types are open tags by default now, I believe that is correct.
In the old days, that would not be the case
That said, even with a closed tag they are slightly different. *
has no constructable error case. It suggests that someone has handled all errors and made them into successes.
NVM, tired morning brain. Also unconstructable if it is a closed union.[]
has an error case with no info in it. So it basically means something failed but we have dropped all information about the failure
I don’t see how [] is an error case with no info given that it is impossible to construct an element of []
Isaac Van Doren said:
What’s the reason for using Task a * rather than Task a []?
the answer varies depending on how it's being used - where are you seeing the type in question? :big_smile:
I don’t see how [] is an error case with no info given that it is impossible to construct an element of []
Ah wait I forgot that [] is open
The first one that comes to mind is Stdout.line
So Task {} *
Yeah sorry, I think Task {} []
and Task {} *
are equivalent if []
is a closed union.
But given roc makes all returned unions into open unions, they quickly become not equivalent.
well also even an open []
is more restrictive than *
because []
says "this must be some sort of tag union"
and you can have errors that aren't tag unions
Definitely problematic to send an opinion union version of Task {} []
back to a platform. It would not have a know layout when generating the platform glue
so if Stdout.line
returned a Task {} []
then you couldn't use it with a Task {} Str
, for example (e.g. if someone used Task.mapErr
to turn an error into a string)
ok I can't intellectually follow the discussion : ) but I gather this is the reason why Task {} * is used ?
That said, even with a closed tag they are slightly different. * has no constructable error case. It suggests that someone has handled all errors and made them into successes.
btw what {} stands for here, why specifically this value?
{}
is just an empty record.
It is common used for kinda a unit type. A concrete type that compiles to nothing.
and you can have errors that aren't tag unions
Aren't there no possible errors for a Task {} *
?
Regardless, isn't there not much you can do with errors if you don't know what type they are?
Aren't there no possible errors for a Task {} *?
This is were it gets confusing depend on how things are worded. Really easy to say something that is easily misinterpreted from what was meant. You are correct that there are no possible constructable errors that match the type Task {} *
.
Richard meant that Task {} *
will unify with any error type. That means I can do:
Note: Stdout.line
is a Task {} *
. I has a *
error case but merges to a Str
error case here just fine.
main =
errStr <- strErrTask |> Task.onErr
Stdout.line "error was: \(errStr)"
strErrTask =
_ <- Stdout.line "Some text" |> Task.await
Task.err "This is a string err"
If I were to redefine Stdout.line
to be Task {} []
, I would get this error:
── TYPE MISMATCH ───────────────────────────────────── examples/helloWorld.roc ─
This 2nd argument to |> has an unexpected type:
11│> _ <- Stdout.line "Some text" |> Task.await
12│> Task.err "This is a string err"
The argument is an anonymous function of type:
{} -> Task * Str
But |> needs its 2nd argument to be:
{} -> Task * []
Task {} []
can only unify with other errors that are tags.
Aha! That makes sense. Thanks for the detailed explanation!
Last updated: Jul 06 2025 at 12:14 UTC