Stream: ideas

Topic: should we have the * type variable?


view this post on Zulip Richard Feldman (Apr 17 2023 at 22:06):

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

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:06):

I haven't put much thought into how to explain this, but I'll give it a shot off the cuff

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:07):

compare these two types:

x : List a
x : List (a, a)

in these two uses, a means something completely different

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:07):

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"

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:08):

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"

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:08):

now compare these types:

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:09):

f : List a -> Nat
f : List a -> a

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:09):

in the first one, a means "this function accepts any list" - it's super flexible

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:09):

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

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:09):

then we have:

f : List a, List a -> Nat

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:10):

now a is even more restrictive; it means that both input lists have to have the same element type

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:11):

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"

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:12):

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"

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:12):

whereas "give it the type HTML star" is clear and easy to have a conversation about

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:13):

basically there's an important distinction between bound and unbound type variables, and I like having a syntactic distinction between them as well

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:14):

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:

view this post on Zulip Luke Boswell (Apr 17 2023 at 22:57):

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;

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...

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:03):

you're right there, and we definitely do not need to have an unbound type variable!

view this post on Zulip Notification Bot (Apr 17 2023 at 23:03):

18 messages were moved here from #ideas > reddit type variable syntax by Richard Feldman.

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:03):

I'm moving this out to a separate discussion!

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:04):

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!)

view this post on Zulip Georges Boris (Apr 17 2023 at 23:09):

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.

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:12):

another situation where this comes up:

» 1 + 1

2 : Num *

...would become:

» 1 + 1

2 : Num a

view this post on Zulip Georges Boris (Apr 17 2023 at 23:18):

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?

view this post on Zulip Georges Boris (Apr 17 2023 at 23:19):

ok, found it.

view this post on Zulip Georges Boris (Apr 17 2023 at 23:21):

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.

view this post on Zulip Georges Boris (Apr 17 2023 at 23:22):

and the ... syntax is already used in the value layer to destructure lists, right?

view this post on Zulip Georges Boris (Apr 17 2023 at 23:23):

I like Html * and (*,*) - feels intuitive to me

view this post on Zulip Georges Boris (Apr 17 2023 at 23:24):

but for extensible stuff I much rather reuse the value syntax and just use {a: Int, ..} and [A, B, ..]

view this post on Zulip Georges Boris (Apr 17 2023 at 23:25):

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...

view this post on Zulip Georges Boris (Apr 17 2023 at 23:26):

maybe the .. as x syntax could also be used in the type layer when we want to do something like the phantom builder pattern?

view this post on Zulip Ayaz Hafiz (Apr 17 2023 at 23:29):

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

view this post on Zulip Ayaz Hafiz (Apr 17 2023 at 23:30):

One idea is to remove * and use _ where it would usually be used

view this post on Zulip Ayaz Hafiz (Apr 17 2023 at 23:31):

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

view this post on Zulip Ayaz Hafiz (Apr 17 2023 at 23:32):

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 *.

view this post on Zulip Georges Boris (Apr 17 2023 at 23:33):

seems less intuitive for beginners as well. but this is completely subjective.

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:49):

yeah it's a subtle distinction

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:51):

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"

view this post on Zulip Georges Boris (Apr 18 2023 at 01:46):

oh - would List * be an empty list? I would read that the same as List a

view this post on Zulip Ayaz Hafiz (Apr 18 2023 at 01:52):

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)

view this post on Zulip Kevin Gillette (Apr 18 2023 at 05:18):

Richard Feldman said:

x : List a

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"

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.

view this post on Zulip Andrei Vasiliu (Apr 18 2023 at 06:01):

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.

view this post on Zulip Andrei Vasiliu (Apr 18 2023 at 06:01):

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.

view this post on Zulip Allan Clark (Apr 18 2023 at 08:41):

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.

view this post on Zulip Anton (Apr 18 2023 at 09:22):

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

view this post on Zulip Richard Feldman (Apr 18 2023 at 11:20):

the type itself it can't really be prohibited - what happens if you put the expression [] into the repl?

view this post on Zulip Richard Feldman (Apr 18 2023 at 11:20):

it has to print a type for that! :big_smile:

view this post on Zulip Anton (Apr 18 2023 at 11:34):

could [] be used for both the value and the type?

view this post on Zulip Richard Feldman (Apr 18 2023 at 11:35):

well, [] already is a type (empty tag union) so we'd need another type for that :sweat_smile:

view this post on Zulip Anton (Apr 18 2023 at 11:35):

Riiight

view this post on Zulip Richard Feldman (Apr 18 2023 at 11:51):

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 a is a kind of default for "can be anything" type variable.

I hadn't even thought of this - great point!

view this post on Zulip Rene Mailaender (Apr 18 2023 at 17:07):

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.

view this post on Zulip Brendan Hansknecht (Apr 18 2023 at 17:30):

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.

view this post on Zulip Allan Clark (Apr 18 2023 at 21:36):

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.

view this post on Zulip Kevin Gillette (Apr 19 2023 at 05:11):

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.

view this post on Zulip Kevin Gillette (Apr 19 2023 at 05:13):

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.

view this post on Zulip Sky Rose (May 09 2023 at 13:44):

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

view this post on Zulip Sky Rose (May 09 2023 at 13:45):

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.

view this post on Zulip Sky Rose (May 09 2023 at 13:47):

(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