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
I haven't put much thought into how to explain this, but I'll give it a shot off the cuff
compare these two types:
x : List a
x : List (a, a)
in these two uses, a means something completely different
in List a it means "this is a list where we can't say anything about its contents; in other words, it's definitely an empty list"
in List (a, a) it means "this is a list where we can say for certain its contents are tuples, and both entries in each tuple has to have the same type"
now compare these types:
f : List a -> Nat
f : List a -> a
in the first one, a means "this function accepts any list" - it's super flexible
in the second one, a means "this function accepts any list, and also it returns something that has the same type as the input list's element" - so there's a bit of a restriction
then we have:
f : List a, List a -> Nat
now a is even more restrictive; it means that both input lists have to have the same element type
what I like about f : List * -> Nat compared to f : List a -> Nat is that it's clearer about "this input type does not matter" - because * always means "does not matter"
it's also easier to talk about out loud - e.g. in Elm, sometimes you'd have heading : Html a which is a weird thing to say out loud "give it the type HTML a but like a lowercase a, so it's a type variable"
whereas "give it the type HTML star" is clear and easy to have a conversation about
basically there's an important distinction between bound and unbound type variables, and I like having a syntactic distinction between them as well
so we absolutely could get rid of *, but if we did, I'd want the reason to be "it wasn't a good idea after all now that we've tried it out" and not "it would facilitate a new alternate syntax idea for records" or something like that :big_smile:
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 in Html 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 verse a, 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 be f : List a, List b -> Nat x : List (*, *) could be x : 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...
you're right there, and we definitely do not need to have an unbound type variable!
18 messages were moved here from #ideas > reddit type variable syntax by Richard Feldman.
I'm moving this out to a separate discussion!
I'm curious what others think - what if we removed the * type variable from the language? (I personally like it but am not super attached to it either, and if others find it more confusing than useful that's an important point to know!)
I appreciate Richard's explanation about * as it gave me new insight about it... I've always seen beginners get confused about Html (lowercase msg) and Html (capitalized) Msg - I'm writing this like this because this how we end up saying it whenever we're _talking_ about it with beginners which is super confusing.
I've always found wildcard type variables to be a pain in Elm when talking about extensible records, these are not really the same as type variables with meaning, they are just saying "extra stuff might be here" and it would be great to standardize that with * instead of someone writing it as a then someone else writing it as x etc etc.
another situation where this comes up:
» 1 + 1
2 : Num *
...would become:
» 1 + 1
2 : Num a
sorry, I couldn't find the syntax in the tutorial so I'll ask here since it's related. what are the syntaxes for extensible records, extensible tagged unions?
ok, found it.
I always found these two syntaxes super weird and I feel it's conflating with the wildcard type variable in things like Num *. it is pretty hard to understand [A, B]* and {a: Int}* as extensible records and tagged unions unless someone explains it to you first.
and the ... syntax is already used in the value layer to destructure lists, right?
I like Html * and (*,*) - feels intuitive to me
but for extensible stuff I much rather reuse the value syntax and just use {a: Int, ..} and [A, B, ..]
just my 2c but I think the wildcard can be nice to explain you don't care about that type at all. has less cognitive load than (a, b) imo...
maybe the .. as x syntax could also be used in the type layer when we want to do something like the phantom builder pattern?
A nice thing about * is that it explicitly signifies a type that "doesn't matter", as mentioned above, in a way that named type variables can't - as soon as you see * you don't need to check if there is another variable of the same name, whereas for type variables you do as Richard mentioned. Maybe in practice this isn't a problem though
One idea is to remove * and use _ where it would usually be used
This has a couple of problems though, because List _ -> List _ could be a type signature for either \x -> x or \x -> [] - but today, the former and latter would be typed as List a -> List a and List * -> List * (or List a -> List b) respectively
Or if you do
f : List a -> List _
f = \x -> x
then actually the type of this is List a -> List a, the a is just inferred on the right-hand side, but if you did List a -> List * the compiler would issue an error. So _ is not a 1-to-1 replacement for *.
seems less intuitive for beginners as well. but this is completely subjective.
yeah it's a subtle distinction
like x : List _ tells you "this is a list and I know what the element type is, but I'm not telling you what it is" whereas x : List * tells you "this is an empty list"
oh - would List * be an empty list? I would read that the same as List a
it is the same as “List a”, I think what Richard means is that the only way to produce a “List *” (or “List a”, where a doesn’t appear elsewhere in the signature) is to yield an empty list, or “crash” (so, the only way to synthesize a concrete List * is via the empty list)
Richard Feldman said:
x : List ain List a it means "this is a list where we can't say anything about its contents; in other words, it's definitely an empty list"
Should we just prohibit a definition like this, since a arguably doesn't provide any value (really it's misleading), and [] should be the canonical/only way to specify that particular fixed value?
Put another way, the a in x : List a essentially means, in context, "_impossible_ to resolve to any element type," whereas the a in f : List a -> Nat means, in context, "_trivial_ to resolve to any element type." With slightly different contexts, the same sub-sequence of tokens means conceptually opposite things.
For what it's worth, as a newbie reading the tutorial, I did not have any trouble understanding that List * is a list of any type while reading the type signature, and it did not seem necessary for me to understand that it has to be an empty list in order to understand the code that used it.
With that said, I'm biased to prefer syntax that is intuitively readable (e.g. Python) and helps understand what the code does, rather than syntax that teaches the language and helps understand how the language works. The latter can always be learned whenever necessary, whereas the former has to be applied a lot more often.
I wrote a blog post about this in Elm.
The main upshot is that I would keep the *, aside from the reasons already given in this thread, the blog post highlights that in a large function, you can accidentally 'capture' a type variable in a let binding. This is easily fixable, but in the worst case results in a pretty cryptic error message. This is exacerbated by the fact that a is a kind of default for "can be anything" type variable.
Should we just prohibit a definition like this, since a arguably doesn't provide any value (really it's misleading), and [] should be the canonical/only way to specify that particular fixed value?
I like this
the type itself it can't really be prohibited - what happens if you put the expression [] into the repl?
it has to print a type for that! :big_smile:
could [] be used for both the value and the type?
well, [] already is a type (empty tag union) so we'd need another type for that :sweat_smile:
Riiight
Allan Clark said:
in a large function, you can accidentally 'capture' a type variable in a let binding. This is easily fixable, but in the worst case results in a pretty cryptic error message. This is exacerbated by the fact that
ais a kind of default for "can be anything" type variable.
I hadn't even thought of this - great point!
ok that is really interesting, because I really like the * syntax and I find it greatly intuitive! For me a lower case type variable shows some kind of relationship. like in map:
map : List a, (a -> b) -> List b
So it still carries some information on. whereas * reads like a "throw away" / "i don't care" / "unknown" type. And therefore it holds some special meaning and brings some value to the language, in my eyes.
I think for examples like map, i think * looks and reads great. I think type variables in general look awkward for records and tag unions.
I had suggested _ since it would be the same as ignoring a value in a pattern match. Star is sometimes used for tuples, but I cannot imagine anyone used to that would be confused by its use for ignored type variables.
Richard Feldman said:
the type itself it can't really be prohibited - what happens if you put the expression
[]into the repl?
To clarify, I mean can we prohibit literally x : List a as being allowed to represent a constant empty-list expression, and instead require use of x : List * ? A code formatter could accept the List a form and rewrite it into List * for constants specifically, but the compiler could reject.
In any case, I do favor the clarity of * over the other alternatives presented. Naming things is hard, and it's unfortunate to force the use of names somewhere that a non-name would serve more clearly.
What if we switched List _ and List *? So List _ meant "don't bind this" and List * meant "figure this out for me".
I had suggested _ since it would be the same as ignoring a value in a pattern match. Star is sometimes used for tuples, but I cannot imagine anyone used to that would be confused by its use for ignored type variables.
This matches the use of _ for unbound variables, where the if you use a _ to bind a variable, you're giving up all knowledge about it and can't reference it again. The concept of the compiler filling in a type parameter for you is only in types, not variables, so it should get the other syntax.
Here's an example where the difference between _ and * matters (written in this proposed swapped way):
# Figure out that this is a Num for me
f : List * -> Str
f = \list ->
list
|> (List.map Num.toStr)
|> Str.joinWith ""
# variable is unbound, can't do anything with the contents of the list
f : List _ -> Bool
f = \list ->
(List.len list) > 0
List _ -> List _ could be a type signature for either \x -> x or \x -> [] - but today, the former and latter would be typed as List a -> List a and List * -> List * (or List a -> List b) respectively
If _ was unbound, then this definitely means List a -> List b, in the same way that (_, _) = (1, "a") can be different types. If you want them to be the same, you should bind them.
If _ means the compiler figures it out, then it could be either but should be whatever the compiler would have chosen if you didn't give an annotation at all.
(Side note: The tutorial doesn't have any mention of _ for having the compiler infer the type. It does have a section on * for "Wildcard" (unbound, matches anything) types.)
Last updated: Jun 16 2026 at 16:19 UTC