Previously we mentioned that Elm has a static type system. The type of every expression is known at compile time, which leads to safer code. If you write a program where you try to divide a boolean type with some number, it won’t even compile. That’s good because it’s better to catch such errors at compile time instead of having your program crash. Everything in Elm has a type, so the compiler can reason quite a lot about your program before compiling it.
Unlike Java or Pascal, Elm has type inference. If we write a number, we don’t have to tell Elm it’s a number. It can infer that on its own, so we don’t have to explicitly write out the types of our functions and expressions to get things done. We covered some of the basics of Elm with only a very superficial glance at types. However, understanding the type system is a very important part of learning Elm.
A type is a kind of label that every expression has. It tells us in
which category of things that expression fits. The expression True
is a
boolean, "hello"
is a string, etc.
Now we’ll use elm-repl to examine the types of some expressions. We’ll do that by typing expressions into elm-repl. After each valid expression, the repl tells us its type. Let’s give it a whirl.
TODO: Add a section on elm-repl, or come up with a different way to accomplish this in the online editor. Maybe type holes?
> 'a'
'a' : Char
> True
True : Bool
> [1, 2]
[1,2] : List number
> (True, 'a')
(True, 'a') : ( Bool, Char )
> 4 == 5
False : Bool
Here we see that typing an expression prints
out the value of that expression followed by :
and its type.
:
is read as “has type of”. Explicit types are always denoted with the
first letter in capital case. 'a'
, as it would seem, has a type of Char
.
It’s not hard to conclude that it stands for character. True
is of a
Bool
type. That makes sense. Examining the type of
[1, 2]
yields a List number
. The lowercase n
in the type of number indicates
a built-in type variable. We’ll get to type variables later in this chapter.
A number can be either an Int
or a Float
, and will concretely become one or
the other depending on how it’s used. Unlike lists, each tuple length has
its own type. So the expression of (True, 'a')
has a type of ( Bool, Char )
,
whereas an expression such as ('a','b','c')
would have the type of
( Char, Char, Char )
. 4 == 5
will always return False
, so its type is Bool
.
Functions also have types. When writing our own functions, we can choose to give them an explicit type declaration. This is generally considered to be good practice. From here on, we’ll give all the functions that we make type declarations. Remember the first function we made previously that doubles a number? Here’s how it looks like with a type declaration.
doubleMe : number -> number
doubleMe x =
x + x
doubleMe
has a type of number -> number
, meaning that it maps
from a number to a number. That’s because it takes one number as a
parameter and returns another as a result.
We didn’t have to give this function a type declaration because
the compiler can infer by itself that it’s a function from a number to a
number but we did anyway. But how do we write out the type of a function
that takes several parameters? Here’s a simple function that takes three
integers and adds them together:
addThree : Int -> Int -> Int -> Int
addThree x y z =
x + y + z
The parameters are separated with ->
and there’s no special distinction
between the parameters and the return type. The return type is the last
item in the declaration and the parameters are the first three. Later on
we’ll see why they’re all just separated with ->
instead of having some
more explicit distinction between the return types and the parameters
like Int, Int, Int -> Int
or something.
If you want to give your function a type declaration but are unsure as to what it should be, you can always just write the function without it and then check it in elm-repl. Functions are expressions too, so it works on them without a problem. To write a multi-line expression in elm-repl, add a backslash to the end of each line you want to continue on the line below, like this.
> addThree x y z = \
| x + y + z
<function> : number -> number -> number -> number
Here’s an overview of some common types.
Int
stands for integer. It’s used for whole numbers. 7
can be an Int
but
7.2
cannot. Int
is bounded, which means that it has a minimum and a
maximum value. The maximum possible value of Int
is
2147483647
and the minimum
is -2147483648
.
Float
is a floating point with the same properties as JavaScript floats.
circumference : Float -> Float
circumference r =
2 * pi * r
toPrint = circumference 4.0
25.132742
Bool
is a boolean type. It can have only two values: True
and False
.
Char
represents a character. It’s denoted by single quotes.
String
represents a string. It’s denoted by double or triple quotes and
can contain many characters. Triple quoted strings are string literals
that can span across multiple lines, with line breaks and whitespace
being preserved in the string.
Tuples are types but they are dependent on their length as well as the
types of their components, so there is theoretically an infinite number
of tuple types, which is too many to cover in this tutorial. Note that
the empty tuple ()
is also a type which can only have a single value: ()
.
This value is read as "unit"
and is the common way to denote an empty
value with no specific meaning.
What do you think the type of the List length
function is? Well, length
takes a list of any type and returns its length, so what could it be? Let’s check!
> List.length
<function> : List a -> Int
Hmmm! What is this a
? Is it
a type? Remember that we previously stated that types are written in
capital case, so it can’t exactly be a type. Because it’s not in capital
case it’s actually a type variable. That means that a
can be of any
type. This is much like generics in other languages, only in Elm
it’s much more powerful because it allows us to easily write very
general functions if they don’t use any specific behavior of the types
in them. Functions that have type variables are called polymorphic
functions. The type declaration of length
states that it takes a list of
any type and returns an Int
representing the length of that list.
Although type variables can have names longer than one character, we usually give them names of a, b, c, d …
Earlier in this chapter we mentioned the number
type variable. This is a
type variable built-in to Elm that specifically represents either an Int
or a Float
, but nothing else. There are other bult-in type variables
representing other important things. comparable
represents types that can
be compared for equality with the ==
operator. This can be String
, Char
,
Int
, Float
, Time
, or a list or tuple containing only comparable values.
toPrint = 4 == 5
False
appendable
represents items that can be appended to each other with the
++
operator. These are String
, List
, and Text
.
toPrint = "Hello" ++ "!"
"Hello!"
Remember Tuple.first
? It returns the first component of a pair. Let’s examine
its type.
> Tuple.first
<function> : ( a1, a2 ) -> a1
We see that first
takes a tuple which contains two types and returns an
element which is of the same type as the pair’s first component. That’s
why we can use first
on a pair that contains any two types. Note that just
because a1
and a2
are different type variables, they don’t have to be
different types. It just states that the first component’s type and the
return value’s type are the same.