elm christmas

How to read type signatures in Elm

←Previous postNext post →

A useful skill indeed!

A 5 min read written by
Ingar Almklov

Being able to read type signatures in Elm will greatly improve your development experience. In this article we will learn how to do just that.

Type inference

Elm has what is called "type inference". This means that if you don't tell Elm what kind of types your program uses it will figure it out anyway. We can see this at work if we drop into the Elm REPL in our favorite terminal.

$ elm repl
---- Elm 0.19.0 ----------------------------------------------------------------
Read <https://elm-lang.org/0.19.0/repl> to learn more: exit, help, imports, etc.
--------------------------------------------------------------------------------
> "Hello"
"Hello" : String
> 155
155 : number
> False
False : Bool

The lines starting with > is our input and the other lines are output. We see that when we write "Hello", Elm replies with "Hello" : String. We can read the : as "has type", which then gives us "Hello" has type String. Similarly, 155 has type number and False has type Bool.

Now let's try some functions.

> greet name = "Hello, " ++ name
<function> : String -> String
> addOne x = x + 1
<function> : number -> number

If we read the -> as "to", we get greet is a function that has type String to String. A bit less formally: greet is a function that takes a String and returns a String. Similarly, addOne is a function that has type number to number.

And for functions that take multiple arguments:

> printAge name age = name ++ " is " ++ String.fromInt age ++ " years old."
<function> : String -> Int -> String

printAge is a function that has type String to number to String. Or, printAge is a function that takes a String and a number and returns a string.

It may look a bit unusual that each argument in the type signature is separated by an arrow. It is this way because of partial application, which we will get back to in a later post. If you read the Wikipedia article it might seem scary and overly mathematical but it is actually incredibly useful! For now you only have to remember that the rightmost value is the one being returned.

Type signatures

Even though Elm can infer the types in our program, it is still very useful to annotate functions and values with types manually. Doing this serves two purposes:

  1. Documentation – when looking at a piece of code it often helps to know exactly what type of values the function is working with.
  2. Easier implementation – when the compiler knows what you are trying to do, it can give you even better error messages if you have an error in your code

It is recommended to always have type signatures on all top-level values and functions. In fact, if you are trying to publish a package on https://package.elm-lang.org/ you are required to have type signatures on all exposed values.

Type variables

If you have read some Elm code, tutorials or even tried to write some Elm yourself, you may have seen type signatures that have lower-case letters in them. Often, these are single-letter names like a, b, c etc., but you may also have seen types like msg or model. What is significant about these is that they start with lower-case letters. This tells the compiler that they are type variables.

To figure out what they are we can look at the update function from The Elm Architecture. Its signature (from Browser.sandbox) is:

update : msg -> model -> model

If we apply what we have learned thus far we can tell that update is a function that takes some msg and a model and returns a model. Since msg and model start with lower-case letters they are type variables. This means that they can be anything you want as long as all the occurrences of a variable is of the same type. So the return type of the variable is the same type as the second argument you give to the function.

Usually in an Elm application you have a custom type for messages called Msg and a type alias for a record that is used for the application state called Model. In this case the concrete type for update would be

update : Msg -> Model -> Model

However, since msg and model are type variables they could just as well be Strings:

update : String -> String -> String

or msg can be a custom type called Power and model a type alias called Metal:

update : Power -> Metal -> Metal

So, we know that all occurrences of a type variable in a signature has to be the same type. What then can you tell me about this function?

identity : a -> a

...

...

...

Well, since all functions in Elm (outside of the Debug module) are pure functions there is only one possible implementation for identity. We know that its return type is the same as the argument given for all types in the whole wide world. Further, since the implementation can't possibly know anything about the argument, the only way to implement this function is to _return the argument_!

identity : a -> a
identity x =
    x

Summary

When reading and writing Elm code it really helps to understand how to read and write type signatures. They often tell you what a function does without having to read its implementation or documentation and they make the compiler give you better error message if you make mistakes.

←Previous postNext post →