Keys to Elm: Type annotations
It’s easy for people to forget how hard they found something when they were beginners. The “Keys to Elm” series tries to teach a few things that will hopefully make learning Elm a bit easier.
Note: This article assumes you have some familiarity with the Elm language. Hopefully, you’ve at least finished the “Types” chapter of the official guide.
Introduction
In Elm, type annotations are lines above functions that tell you what the type of each argument is and also the function’s return type. Here’s an example of a function with a type annotation:
save : String -> Cmd msg -- This is the type annotation
save markdown = -- The actual function's definition starts here
Download.string "draft.md" "text/markdown" markdown
What you may be able to intuit is that the argument markdown
is of String
type. The return type of the function is the one at the very end of the type annotation. In this particular case, the save
function returns aCmd msg
.
You’ll encounter type annotations in the documentation and in code examples. Just being able to make sense of the type annotations will have you well on your way to understanding code, writing it, and debugging it.
Examples
Example 1
Let’s start with a simple example of a type annotation:
round : Float -> Int
The above means that round
is a function that takes a Float
as an argument and returns an Int
value. As mentioned above, the return type of the function is the very last type in the annotation.
Example 2
Here’s another:
modBy : Int -> Int -> Int
This just means that the modBy
function takes 2 arguments, both Int
s, and outputs an Int
. Again, the last type of the type annotation tells us what the type of the function’s output is.
Example 3
The difficulty level ups to intermediate in this one:
identity : a -> a
Notice how the a
is lower-cased? This means that it can be of any type — an Int
, a Float
, a Char
, a String
, a user-defined type. Anything at all. They are therefore called type variables.
Further, the fact that the input and output of the function is type a
means that the function will return a value that is of the same type as the argument it was given. To be clear: this does not mean that if you send in a 20
, you will get back a 20
; all that we can tell from the signature is that sending in an Int
will result in an Int
.
Example 4
A slightly more complicated example:
always : a -> b -> a
Here, we learn from the signature that always
is a function that takes two arguments: one of a
type and one of b
type.
From the previous example, we learned that a
and b
could be anything: an Int
, a Float
, a Bool
, a String
, a user-defined type — anything at all.
However, since the output of the function is of type a
, it means that the function will return a value that will be of the same type as the first argument.
Example 5
Ready to up the ante? Here goes:
map : (a -> b) -> List a -> List b
The second argument to the map
function is List a
, meaning it is a list of a
type elements. The output of the map
function is a list of b
type values.
The first argument to the map
function is “(a -> b)
”. The parentheses signifies that it is a function. In other words, the map
function expects a function as its first argument. Specifically, it wants a function that takes an argument of type a
and returns a value of type b
.
Now that the type signature is clear to you, it doesn’t take much to deduce that the map
function takes a list of a
values and passes each of them through the (a -> b)
function to get a list of b
values, which is sent out as the result of the map
function.
Example 6
Now, take a look at a special case. Here’s the onClick
function’s signature:
onClick : msg -> Attribute msg
You might have noticed that msg
is lower-cased, just like a
and b
were in the previous examples. As we’ve learned so far, lower-cased names like a
and b
mean that the “type variable” can be of any type. However, as per convention in Elm, we are expected to send inMsg
s here. The return value of the function will be an Attribute msg
, meaning that the value you sent in will come out wrapped in an Attribute
.
Note: I haven’t gone into what
identity
oralways
or any of the functions in the above examples do because that’ll distract us from the task of learning to read types.
Using this skill
Prevent errors
Elm will infer types even if you don’t annotate your code and your code will not compile if the wrong type of arguments are used. Start fixing errors in your code by checking the type annotations, especially if you are a beginner or are using functions you aren’t familiar with.
Find appropriate functions to use
Let’s suppose you wanted to make HTTP calls. Here’s the type signature of the Http.get
function:
Http.get :
{ url : String
, expect : Expect msg
}
-> Cmd msg
The Http.get
function wants a record with the second field of Expect msg
type. Looking through the documentation, you’ll see that there are 4 functions whose outputs are of the Expect msg
type:
Http.expectWhatever : (Result Error () -> msg) -> Expect msg
Http.expectString : (Result Error String -> msg) -> Expect msg
Http.expectJson : (Result Error a -> msg) -> Decoder a -> Expect msg
Http.expectBytes : (Result Error a -> msg) -> Decoder a -> Expect msg
So, we just pick one of these functions depending on whether we want String
, JSON
or Bytes
. And, then we plug it into the record of the Http.get
call. Like so:
-- Assume that GotSampleText is a Msg in your app
Http.get :
{ url : “https://www.example.com/sampletext
, expect : Http.expectString GotSampleText
}
Substitute values with functions
Since Elm is a functional language, you can pass around functions just as easily as you pass around values. In other words, if a function expects an argument of a certain type, you aren’t restricted to sending in only a value of that type to it, you can just as easily send in a second function whose return type will be the same as the expected type; values and functions are interchangeable as long as the types match.
Suppose we have two functions:
-- Here's the signature of a hypothetical function that takes a number
-- and outputs something like, "The total is $70"
whatsTheTotal : Int -> String
-- And, here's the signature of another hypothetical function that
-- totals a list of numbers
totalThese : List Int -> Int
-- You could use the first function in this way:
whatsTheTotal 70
-- Or, you could use it this way:
whatsTheTotal (totalThese [10, 20, 40])
Since the return type of totalThese
is an Int
(the same as the input argument for whatsTheTotal
), you can simply pass in the function itself. There’s no need to invoke functions, put their values into a variable, and then supply that value into another function. It’s all done in a single step.
Conclusion
No, people don’t learn to read types overnight. It’ll take practice. However, I hope I have you convinced to pay more attention to the type annotations. Cheers!
If you’d like, you can support my caffeine habit with a virtual coffee!