Hi all. I just wrote my first bit of Roc code, and I'd appreciate any feedback, if people don't mind. I'm trying to set up a heterogeneous list where different types of elements (each with their own content tag) can be stored. This means that ultimately, the list of elements might have a type signature consisting of a a very large tag union. I don't know whether this is considered bad form or a nice way to get self-documenting code. I tried to make a very abstract "Element" type alias consisting of a record with two open tag unions, but it looks like you can't put open tag unions in a type alias?
My other question involves the last line of my code, which I thought would result in something that was either Err ... or Ok (Image ...), but it didn't end up as specific as I expected. So I'm curious how I could improve it.
Beyond that, does this code generally make sense? Obviously, it's just a start on an overall program. Thanks.
# Element: {world: []*, content: []*} <---- This does not work.
# elements: List Element
# Heterogeneous list of elements. In practice, there would be many types of elements
# that could appear here.
elements = []
|> List.append {world: None, content: Empty}
|> List.append {world: Some, content: Integer 3}
|> List.append {world: None, content: Image (120,120)}
# Check whether an element has an Image as its content
isImage = \element -> when element is
{content: Image _} -> Bool.true
_ -> Bool.false
# Attempt to downcast an element to one that has an Image as its content
asImage = \element -> when element is
{content: Image _} as ele -> Ok ele
_ -> Err FailedToCast
# Takes a list of elements and a function that attempts to downcast an element
# to a particular type. Returns the first element that has been successfully downcast.
findFirstSuccess = \elementList, asSomething -> when elementList is
[element, .. as tail] -> when asSomething element is
Ok a -> Ok a
Err _ -> findFirstSuccess tail asSomething
[] -> Err NotFound
# Find the first element that is an image
firstImage = List.findFirst elements isImage
# Find the first element that has been successfully downcast to one containing an image.
# This should be more specific than firstImage, but currently it is not.
betterFirstImage = findFirstSuccess elements asImage
The first line would need to be this to work:
Element a b : { world: []a, content: []b }
Representing a list of tags might be completely reasonable. Generally speaking, it is less performance than some other solutions, but it can often be the most convenient solution.
For the last part, this is a semi-common confusion. Roc does not have any sort of gradual typing.
A [ Empty, Integer I64, Image (I64, I64) ]
will always be a [ Empty, Integer I64, Image (I64, I64) ]
You have to explicitly create a new type to separate it from that union. Generally, I do this by fully unwrapping the type. So just return the inner (I64, I64)
instead of the full tag. That said, you could rewrap it and return a [ Image (I64, I64) ]
if you want.
{content: Image i} -> Ok {content: Image i}
That code completely separated the two image tags. On the left is a [ Empty, Integer I64, Image (I64, I64) ]
. On the right is a newly constructed [ Image (I64, I64) ]
@Brendan Hansknecht I appreciate the feedback. Regarding the first part, I tried doing the following:
Element a b: {world: []a, content: []b}
elements: List (Element * *)
elements = []
|> List.append {world: None, content: Empty}
|> List.append {world: Some, content: Integer 3}
|> List.append {world: None, content: Image (120,120)}
But this is giving me an error that seems to suggest I'm creating a closed tag union with my type signature. I thought {:world []*, :content []*}
would mean open tag unions, so I must be missing something. In any case, if having a large tag union is reasonable, then perhaps I'm better off not providing a type signature for elements anyway, since that means I can see the auto-generated signature that shows all the possible types that tag could be, based on all the code that's being called. (To be clear, I'm trying to avoid needing to specify all the possible tags ahead of time and in one place, so that it's easy to add new tags in new files in the future).
Thanks.
Can you just use _
and let the compiler infer it?
elements: List (Element _ _)
@Luke Boswell That does work, thank you, though I don't understand the difference between using a wildcard, which could mean anything, and using a *, which I thought could also mean anything in a type signature.
I guess it's something like...the wildcard is telling the compiler to fill in the details, whereas the * is telling the compiler something, but I don't fully understand what, or why []*
wouldn't be consistent with any possible tag.
It would be really great if someone with decent type theory knowledge could write up a guide around the use of wildcard and types in general. This is a bit of a FAQ and it's sometimes confusing.
My laymans understanding is that *
in this position is saying match with "anything" which effectively means nothing because there are no types that will unify with all tags unions except the empty one []
.
https://roc.zulipchat.com/#narrow/channel/231634-beginners/topic/templates.3F/near/407225577
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.
Tutorial link https://www.roc-lang.org/tutorial#wildcard-type
I'm just digging up references at this point... trying to clarify for myself
Brendan Hansknecht said:
It could be replaced, but seeing
List a
is meant to have a meaning.a
is a type variable that is expected to be matched with other locations. By writinga
it is kinda like specifying that you care about the type.
(List a, a)
means that I care about the element type of the list. It must match the other value in the tuple.Seeing
(List a, b, List c, d)
is just noise. None of those type variable have any meaning. As a reader of the type signiture, I expect to see the type variables used elsewhere, but they aren't.Seeing a
*
is a clear signal that the type can not matter. There is no way it can be depended on or matched at all.
Richard Feldman said:
Luke Boswell said:
Would it be possible to remove the
*
altogether from the language and just use lowercase letters?so this is how Elm does it, and I specifically wanted to add it because I don't like how lowercase letters are overloaded
Richard provides a good explanation in this ^^^ thread
I think it's was suggested that we get rid of * at some point, and I think that's still the right move. I think List.len : List a -> U64
is basically as clear as List.len : List * -> U64
. We lose the communication of "this type should be ignored", but it leads to so much confusion...
Luke Boswell said:
Thank you for explaning this. I really appreciate it. I'm happy with the status-quo, however, my goal here is to provide an alternative argument which may improve Roc by simplifying it a little.
From my experience I think the
a
inHtml a
is easier to understand. Once I learnt that lowercase letters were type variables it felt natural and is used just like other variables.I find the
*
confusing as it is a special case and sometimes used, sometimes not, it hasn't been clear when to use it versea
,b
,c
etc. Your explanation here has helped me see the intent behind it, as it is an unbound type variable and can mean anything and two*
s are not equal/must be different.However, isn't is possible to use different letters to show this same thing more explicitely? I.e. that these types must be different? Do we need to have an unbound type variable?
For example;
f : List *, List * -> Nat
could bef : List a, List b -> Nat
x : List (*, *)
could bex : List (a, b)
basically there's an important distinction between bound and unbound type variables, and I like having a syntactic distinction between them as well
This leads me to think that maybe I don't fully understand it though...
I still feel this way, even having stared at roc code at lot since then. The *
still feels a little magic.
Anyway... sorry for taking this thread of course @misterdrgn
Personally I think we should only allow *
on function inputs. Ban it in all other cases. Then it can only be used in the clear case.
It would mean: Pass me anything. I know nothing about the thing, but will manage anyway.
*
on something concrete means that you must know nothing about the concrete type. Which essentially means it must be an empty or none value. But restricting a concrete type to only the empty or none value is not useful.
That issue getting hit here. Writing Element * *
on a concrete type means that the element type contains tag unions that you know nothing about. That clearly is not true. You know they contain Empty
, Integer
, and Image
. That is why the types don't work out. For the wanted flexibility, Element _ _
is the correct type.
@Brendan Hansknecht That makes sense, thank you.
@Brendan Hansknecht One more question, if you don't mind. I threw together the following function, which doesn't work for reasons that are pretty obvious.
# Takes a list of elements and a function that takes a certain kind of element and returns a Bool.
# Attempts to cast each element as a type that matches the input signature of the function.
# If the cast succeeds, then returns that element if calling the function on it returns true.
findElement: List (Element _ _), ({}a -> Bool) -> {}a
findElement = \elementList, test -> when elementList is
[element, .. as tail] -> when element is
{}a as ele -> Ok ele
Err _ -> findExp tail asSomething
[] -> Err NotFound
# Example of how this might be used. The anonymous function should be called on any Image elements,
# whereas other elements should be ignored.
myImage = findElement elements (\{content: Image size} -> size == (120,140))
I can't match element to {}a
because within the body of the function, there's no way of knowing what "a" is. But does this idea make sense? I want to be able to take what in some languages would be a "partial function," that is a function that only works when called on a subset of elements. Then I want to find elements that match the input it is expecting, and when there is a match, I can try calling the function on those elements.
I'm not sure whether this concept makes sense in Roc. If not, I can write a longer piece of code that has the desired effect, for example the asImage function from my original code snippet. But it would be pretty cool if I could define a more abstract function that didn't require defining an asX for every possible element type.
I'd be open to trying out changing the stdlib to no longer use *
and the automatically generated types (e.g. in the repl and error messages) to no longer use it
(which is to say, sort of soft-deprecating it and planning to take it out)
I'd like to get a feel for how much of the confusion is about *
specifically versus unbound type variables
for example, today if you put []
into the repl it says the inferred type is List *
. If instead it starts printing List a
are people more, less, or the same amount of confused about why it said that and what that means?
I don't know, but I'm open to trying it
Brendan Hansknecht said:
Personally I think we should only allow
*
on function inputs. Ban it in all other cases. Then it can only be used in the clear case.It would mean: Pass me anything. I know nothing about the thing, but will manage anyway.
What about trying this? is this kind of a mid way point to try?
A downside of having *
is that we have to explain when they are used over named type variables, and it seems like that downside would get worse if we not only had to explain the difference (like today) but additionally had to explain why unbound type variables in arguments use *
but unbound type variables in other positions don't.
Also, what counts as "in an argument" is tricky - e.g. what about foo : ({} -> Num *) -> Str
- does that count as "in an argument" because it's in foo
's first argument? Or does it not count because foo
's first argument happens to be a function, and the *
happens to be in the return position of that function? I think the spirit of the idea would suggest that this should not be an allowed use of *
, but it makes the rule even more complicated to learn.
Overall, I think we should either have special syntax for unbound type variables or we shouldn't.
and my current intuition is that we probably shouldn't, in that:
If you don't want it to be arbitrarily restricted like I mentioned then I assume eventual removal will be a the cleanest for beginner understanding. We shall see though. Just need to add a formatter and tutorial update to test it out
misterdrgn said:
Brendan Hansknecht One more question, if you don't mind. I threw together the following function, which doesn't work for reasons that are pretty obvious.
# Takes a list of elements and a function that takes a certain kind of element and returns a Bool. # Attempts to cast each element as a type that matches the input signature of the function. # If the cast succeeds, then returns that element if calling the function on it returns true. findElement: List (Element _ _), ({}a -> Bool) -> {}a findElement = \elementList, test -> when elementList is [element, .. as tail] -> when element is {}a as ele -> Ok ele Err _ -> findExp tail asSomething [] -> Err NotFound # Example of how this might be used. The anonymous function should be called on any Image elements, # whereas other elements should be ignored. myImage = findElement elements (\{content: Image size} -> size == (120,140))
This concept doesn't make sense in roc. Either all values in the list match a
or it is a type error. This is fundamentally how unification works. So that would never type check. So the when ... is
would need to move into the lambda.
I think that rocs tags are so flexible that it often leads to confusion for beginners. I'm not sure what other language you know, but in whatever other language, roughly imagine a tag union as an enum. Imagine defining an enum with thee variants:
enum MyEnum {
Empty,
Integer,
Image,
}
When trying anything that interacts with the enum, you type it as MyEnum
. Nothing would ever be typed as Image
. Image
is not a type. It is simply part of the MyEnum
type. The same goes with all tags in roc. It just gets confusing cause they are structural and automatically unify. Makes them feel exceptionally flexible, but they are still just enums (potentially with an attached payload).
Last updated: Jul 05 2025 at 12:14 UTC