Why is "number" not allowed in my type alias of a record?

I’m learning Elm and I’m on this page of the guide. I tried typing

type alias Drink = { brand : String, color : String, liters : number }

but I got an Unbound Type Variable error:

The Drink type alias uses an unbound type variable number in its definition:

type alias Drink = { brand : String, color : String, liters : number }
                                                             ^^^^^^

You probably need to change the declaration to something like this:

type alias Drink number = ...

Why? Well, imagine one Drink where number is an Int and another where it is
a Bool. When we explicitly list the type variables, the type checker can see
that they are actually different types.

Having just read this page of the guide, I don’t understand this error. Isn’t number only constrained to Int and Float? And why does the number type variable work in other contexts, like for the negate : number -> number function?

Edit: While typing this up, I realized that the last paragraph of the error message was a general statement and not saying that number can be a Bool. It may be starting to click to me that when we create instances of type Drink (is “instances of type Drink” the right verbiage?), we need to know whether the liters field is an Int or Float. But why would that need to be so explicit?

2 Likes

you need this here because Elm does not have higher ranks.

Basically when you have negate : number -> number there is a for all types number hidden in the definition. You can think of this as an additional parameter (a type) you need give to get a concrete implementation.
And for number there is also a hidden constraint in there (but that is beside the point):

Same for types type alias Drink number tells the type checker that there is a Drink type alias for all number types you can choose (Int and Float as you already found out) - but you have to give it.

something like

type alias Drink = { liters : number }

would mean: you have one type with a field liters that has the `for all number’ inside

type alias Drink = { liters : forall number. number }

there are type-systems/programming language out there that can do that and the feature is called Rank-2 types or in general rank-n types.

If you want to know more there is a good overview on that on the Haskell Wiki on RankNTypes

I think that is a bug in the error message.

Instead of

“and another where it is a Bool.”,

it should read

“and another where it is a Float.”

$ elm repl
> type alias Drink number = { liters: number }

> Drink
<function> : number -> Drink number

> Drink 1.234
{ liters = 1.234 } : Drink Float

> Drink 1
{ liters = 1 } : Drink number

That last line is interesting. I expected Elm to say { liters = 1 } : Drink Int.

Welcome @zdgra :wave:

A problem with that scenario is that there are two different things in the compiler, which both happen to be named number. No wonder this can be confusing.

So what are these different things? One of them is the type class number, which is, as you pointed out, explained at https://guide.elm-lang.org/types/reading_types.html
The other number is a variable created in that type alias declaration. The name does not make any difference here. It is not related to the number type class.

We can instantiate a Drink number alias using any type for that variable.
For example, this code works:

type alias Drink number =
    { brand : String, color : String, liters : number }


toggleLiters : Drink Bool -> Drink Bool
toggleLiters drink =
    { drink | liters = not drink.liters }
---- Elm 0.19.1 ----------------------------------------------------------------
Say :help for help and :exit to exit! More at <https://elm-lang.org/0.19.1/repl>
--------------------------------------------------------------------------------
> type alias Drink number = { brand : String, color : String, liters : number }

> toggleLiters : Drink Bool -> Drink Bool
| toggleLiters drink =    { drink | liters = not drink.liters }
|
<function> : Drink Bool -> Drink Bool

> toggleLiters { brand = "Campari", color = "crimson red", liters = False }
{ brand = "Campari", color = "crimson red", liters = True }
    : Drink Bool

So yes, that number could also become a Bool in the instantiation of that type.

In the context of a type annotation for a function, yes, but not in the type alias declaration.

For example, we can create a function as follows, supporting both Drink Int and Drink Float:

incrementLiters : Drink number -> Drink number
incrementLiters drink =
    { drink | liters = drink.liters + 1 }

And in a function with that type, we can provoke a type mismatch error by adding an operation that works with Float and not with Int:

provokeTypeMismatch : Drink number -> Drink number
provokeTypeMismatch drink =
    { drink | liters = drink.liters / 1 }

Then we get this error message:

The left side of (/) must be a Float, but instead I am seeing:

    number

Hint: The `number` in your type annotation is saying that ints AND floats can
flow through, but your code is saying it specifically wants a `Float` value.
Maybe change your type annotation to be more specific? Maybe change the code to
be more general?

Read <https://elm-lang.org/0.19.1/type-annotations> for more advice!
2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.