Tuesday, September 18, 2007

Lesson 4: Types

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:

  1. Always return True no matter what the input is

  2. Always return False no matter what the input is

  3. Always return the input value. i.e., for True return True and for False return False

  4. For True return False and for False return True

  5. 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.

9 comments:

Leah said...

I'm taking a class in functional programming that is going way over my head, and this blog is exactly what I needed to catch me up to speed in the Haskell stuff I should have learned weeks ago. It's been a while since you updated...planning on doing more?

Anonymous said...

Next lessons?

Thunder said...

When I try to declare a new datatype in ghci, I got the following error:

Prelude> data Bool = True | False
*interactive*:1:0: parse error on input `data'

Can you help me? thanks!

Anonymous said...

These lessons are great, it's clearly been a while since the last update but nonetheless I would encourage you to continue! Very useful to all beginners.

Rui Meira said...

really nice tutorial, you should continue it ;)

Anonymous said...

looking for more tutorials from you...nice work

Samuel said...

Keep on writing!
This blog is great for beginners!
(and your hands on approach is a very good complement to other resources like learnyouahaskell)

John Creighton said...

Thunder, I get the same error. I'm just going to try and create stuff in a separate file and then load it into ghci.

Anonymous said...

the thing i like about your writing is the structure which is more inclined on concept of learn by doing. This is the method i like to learn with.