Overview
In this lesson, we will introduce the type system. In Haskell, every expression can be classified by its type. We can use GHCi to look at the type signature of expressions by using the :t command:
Prelude> :t 'r'
'r' :: Char
Prelude> :t True
True :: Bool
We see that the character
'r'
has the type Char
, and boolean value True
has the type Bool
.Functions
Types are more exciting once we start looking at functions, so let's look at the type of the
not
function:
Prelude> :t not
not :: Bool -> Bool
We can read that line as follows:
not
is the expression whose type we want to know. In this case the expression is the variable not
.::
indicates that what follows is a type signature.Bool -> Bool
tells us that not
is bound to a pure function that takes a value of type Bool
and returns a value of type Bool
.We can easily try out the
not
function in GHCi:
Prelude> not True
False
Prelude> not False
True
Prelude>
We can also ask GHCi for the type of the expression
not True
Prelude> :t not True
not True :: Bool
Prelude>
Note that GHCi does not run
not True
and look at the result. Instead it looks at just the types of not
and True
and calculates what the resulting type must be.Type Errors
Let's see what happens if we try to call
not
on a character. I created a file name Test.hs which contains just the following line:test = (not 'r')
When I try to load it into GHCi, I get the following error:
Prelude> :load "/tmp/Test.hs"
[1 of 1] Compiling Main ( /tmp/Test.hs, interpreted )
/tmp/Test.hs:1:12:
Couldn't match expected type `Bool' against inferred type `Char'
In the first argument of `not', namely 'r'
In the expression: (not 'r')
In the definition of `test': test = (not 'r')
Failed, modules loaded: none.
Prelude>
This error indicates that the compiler found a problem when it was checking the types. The error message gives us lots of information about exactly where it ran into trouble. In most cases when I get a type error, looking at just the following information is enough to quickly see and fix the problem:
Prelude> :load "/tmp/Test.hs"
[1 of 1] Compiling Main ( /tmp/Test.hs, interpreted )
/tmp/Test.hs:1:12:
Couldn't match expected type `Bool' against inferred type `Char'
In the first argument of `not', namely 'r'
In the expression: (not 'r')
In the definition of `test': test = (not 'r')
Failed, modules loaded: none.
Prelude>
But sometimes the error is not so obvious and the extra information is useful. Let's look more closely:
/tmp/Test.hs:1:12:
This tells us that it ran into a problem in /tmp/Test.hs at line 1, column 12, i.e., when it sees
'r'
.
Couldn't match expected type `Bool' against inferred type `Char'
GHCi was expecting to find a
Bool
value, (e.g., True
or False
), but instead it found a Char
value.
In the first argument of `not', namely 'r'
In the expression: (not 'r')
In the definition of `test': test = (not 'r')
The next three lines give us a contextual description of where the error occurred. Although the first line already told us the exact file, line number, and column, the context information can sometimes help you understand how the compiler sees the code better. For example, you will can see that it implicitly added parentheses in this expression
(not 'r')
, and that it thinks (correctly) that 'r'
is the first argument to the function not
.Creating Your Own Types
The
Bool
type can be defined like this:data Bool
= False
| True
The keyword
data
indicates that we are declaring a new algebraic data type. Bool
is the name of the type, also known as a type constructor. After the =
we have two data constructors: False
and True
. The |
is a separator that is required between constructors.That is all that is required to define the type. We can use it like this:
module Main where
import Prelude hiding (Bool(..)) -- this hides the predefined version of Bool
data Bool = False | True
main =
case True of
True -> putStrLn "True"
False -> putStrLn "False"
We see the
case
statement that we introduced last lesson. This time instead of pattern matching on a String
we are matching on a Bool
. Cool Stuff We Learned Today
Today we saw some really cool stuff!
Type Inference
The first thing we saw is that the compiler is able to infer the types of expressions automatically. This inference is not limited to just predefined values like
not
or True
. The compiler was able to infer the types of new expressions that we created, like not True
.Static Type Checking
In addition to inferring the types of expressions, the compiler also checked that we used expressions in a sensible way at compile time. For example, it noticed that we tried to pass
'r'
to the not
.Static type checking eliminates a whole category of common bugs by preventing you from calling a function with a nonsense argument. It is also useful if you change the arguments that a function takes. The compiler will let you know all the places that need to be updated to reflect the change. This is especially useful if you change a library function that is used by lots of applications. When you later try to rebuild those applications, you won't have to remember if they use any functions that have changed -- the compiler will tell you.
Due to type inference, we get these benefits for free -- we don't have to run the code, we don't have to write unit tests to test the code, and we don't have to explicitly declare our types, we just have to compile it, or load it into the interpreter.
New Types
We also learned how easy it is to create new types in Haskell.
We Know A Lot About not
We saw earlier that
not
is a pure function with the type signature Bool -> Bool
. It turns out that we know an awful lot about the function just from the type signature.We know this function is pure because all functions in Haskell are pure by default. A pure function always produces the same output when given the same input. This is because pure function has no way to remember information from one call to another and it has no way to access external IO resources, such as the disk, keyboard, a random number generator, etc. Therefore, there is no way to return a different result, when presented with the same input. You are already familiar with pure functions from basic arithmetic; functions like + and - are pure functions. 1 + 1 always equals 2 no matter what calculations you have done in the past.
We also know that this function takes a
Bool
and returns a Bool
. Combining purity and the type signature
Bool -> Bool
, we can see that not
could only do one of five possibly things:- Always return True no matter what the input is
- Always return False no matter what the input is
- Always return the input value. i.e., for
True
returnTrue
and forFalse
returnFalse
- For
True
returnFalse
and forFalse
returnTrue
- Never return at all
Since we tried out
not
interactively, we know it is option 4:
Prelude> not True
False
Prelude> not False
True
Prelude>
Later, we will see how purity makes it easy to do automated unit testing.
Additional Notes
There a few simple rules you need to know when declaring a new data type.
data
Declarations Must Be at the Top Level
You can not declare new data types inside functions or inside anything else. They must always be declared at what is called the top-level.
Case matters
Type and data constructors must always start with an uppercase letter, followed by zero or more upper or lower case letters or numbers.
Exercise
Your simple exercise for today is to create your own data type, and use it in a case statement, similar to the Bool example given above.
Preview
If you experimented with the
:t
command, you may have found that some values return some unusual looking results, for example, we see that "hello"
is a list of characters:
Prelude> :t "hello"
"hello" :: [Char]
We will learn more about lists very soon. The number
1
has a really funny looking type:
Prelude> :t 1
1 :: (Num t) => t
That is because we want to be able to use
1
to represent values of several different types. For example, an Integer
or a Float
. We will learn more about this type signature when we learn about polymorphism and type classes. Closing
Today's lesson had lots of technical terms in it. Don't worry too much about about the exact meaning of the words, and don't worry about trying to remember them all at once. These same terms will come up again and again in future lessons and I will continue to try to make their meanings obvious from the context. Once you have more experience with Haskell, it will be easier to give concrete definitions of the terms.