2. Types: what’s the big deal?
If you’ve been following along and trying out homework, you’re
probably intimately familiar with the facts that
Haskell is a statically typed language (it checks types before
we run), and
and it’s quite strict about this (it’s strongly typed).
3. What does “strongly typed” actually mean?
Definition
A strong type system guarantees that a program cannot contain
certain kinds of errors.
Strength and dynamism are more or less independent concepts in a
type system.
There are degrees of both strength and dynamism in type systems,
and often disagreement on both, so the likelihood of confusion
when talking to someone about types is high.
4. Familiar types
Haskell provides a number of types similar to those you’ll have seen
in other languages:
Int is a fixed-width signed integer type, usually the system’s
native word size (32 or 64 bits).
Char is a Unicode character.
Double is the double-precision floating point integer type,
usually 64 bits.
Integer is a signed integer type of unbounded size
(unbounded if you have infinite memory, that is).
Bool is the Boolean type, with possible values True and
False.
5. Expressions and types
Every expression (and hence every value) in Haskell has a type.
The expression ’a’ has the type Char. We can write this as
follows:
a : : Char
a = ’a ’
You can read the syntax “ :: ” as “has the type,” so:
The notation “a :: Char” is called a type signature, and
has the meaning “the expression named a has the type Char.”
6. Type inference
Have you noticed that most of the homework answers submitted so
far contain no type signatures?
This is because the Haskell compiler infers what the type of every
expression and definition is.
When you write a = ’a’, the compiler knows that the expression
’a’ can only have the type Char, so the variable a must have this
type too.
7. Type inference and concision
Unlike other statically typed languages you may be familiar with,
hand-written type annotations are optional1 in Haskell.
This is one of the big factors that lets us write amazingly concise
code:
We don’t need to tell the compiler what our types are when it
can figure this out for itself.
1
Well, they’re almost always optional.
8. Functions have types, too
f a c t : : I n t e g e r −> I n t e g e r
fact n | n < 0 = error ” negative ! ”
| o t h e r w i s e = go n
where go 0 = 1
go i = i ∗ go ( i −1)
A “−>” symbol in a signature denotes the type of a function.
On the left of the −> is the type of the argument.
The type on the right is that of the result.
Read the arrow and its neighbours as “this is a function from the
type Integer to the type Integer.”
9. Functions with multiple arguments
Suppose we have a function with more than one argument. Here’s
how we write its type signature.
c o n s : : Char −> S t r i n g −> S t r i n g
cons x xs = x : xs
Every item in the chain of arrows, except the last, is the type of
the argument at that position. The last item in the chain is the
type of the result.
What is the type of x above?
What is the type of xs?
What is the function’s result type?
10. Tuples
An easy way to create a collection of values is using a tuple.
b l a r g l e : : ( Char , I n t , Bool )
b l a r g l e = ( ’ a ’ , 42 , False )
The type signature (optional, as usual!) indicates what the type of
each element in the tuple is.
A tuple of two elements is usually called a pair.
Tuples with three elements are often called triples.
Larger tuples are 4-tuples, 5-tuples, and in general, n-tuples.
(But 4-tuples and larger are very rare in the wild.)
There is no one-tuple type (it would be redundant).
11. More about tuples
It should be obvious that the following two tuple types can express
different numbers of values:
(Bool, Bool)
(Bool, Bool, Char)
As far as the type checker is concerned, they are indeed completely
distinct types.
This suggests that the following expressions might have different
types:
(True, ’a’)
(’a’, True)
What do you think?
12. Functions on tuples
Pairs are so common in Haskell that there are built-in functions,
fst and snd for accessing the first and second elements of a pair:
fst (1 , ’a ’ )
== 1
>
snd ( 1 , ’ a ’ )
== ’ a ’
>
13. Pattern matching on tuples
For larger tuples, built-in accessor functions are not supplied, since
those types are used much less often.
You can define your own functions using pattern matching, and the
syntax is as you’d expect:
fst3 (a , b , c) = a
snd3 ( a , b , c ) = b
thd3 ( a , b , c ) = c
In practice, it’s more common to pattern-match than to define and
use accessor functions.
14. The list type
The ghci interpreter has a very useful command, :type, for
figuring out what the type of an expression or function is.
Here’s an example:
Prelude> :type lines
lines :: String -> [String]
As this suggests, the notation for “list of String” is [String].
And as the notation suggests, every element in a list must have the
same type.
15. Type synonyms
Recall that a Haskell string is just a list of characters.
The type of a list of characters is [Char], but we often see String
in type signatures.
There is no magic at work here: String is just a synonym for
[Char]. The compiler treats them identically.
In fact, we can introduce our own type synonyms at will:
type S t r e e t N u m b e r = I n t
type StreetName = S t r i n g
type A d d r e s s = ( StreetNumber , StreetName )
(Think typedef, C programmers!)
16. Functions over lists
Type synonyms are only mildly interesting; I mentioned them
mainly to bring the String ≡ [Char] equivalence to the fore.
Here’s why: notice that most functions on lists don’t seem to care
what types the elements of those lists have:
drop 3 [ 0 . . 1 0 ]
== [ 3 , 4 , 5 , 6 , 7 ]
>
drop 5 ” a b a c i n a t e ”
== ” n a t e ”
>
What’s going on here?
17. Polymorphism
Consider the following function definition:
take n | n <= 0 = []
take [] = []
take n ( x : x s ) = x : take ( n−1) x s
The take function never inspects the elements of the list. It
neither knows nor cares what they are.
We call functions like this polymorphic over the list’s elements.
18. Polymorphic type signatures
How do we write the type signature for take?
take : : I n t −> [ a ] −> [ a ]
That name “a” above, inside the list brackets, is a type variable.
A type variable lets us say “I don’t know or care what concrete
type2 will be used in this position, so here’s a placeholder.”
2
“Concrete type”? Think Int, Char, etc.
19. Multiple type variables
We are not limited to a single type variable in a type signature.
triple : : a −> b −> c −> ( a , b , c )
triple x y z = (x , y , z)
This means:
The first argument could have any type a.
The second argument could have any type b. This type could
be the same as, or different to, a.
And the third? You get the idea.
20. Functions as parameters
Suppose we want to inspect every element of a list, and drop those
that do not suit some criterion.
How should we write the type signature of a function that checks
an element of the list?
3
Fancy language for “function returning a Bool.”
21. Functions as parameters
Suppose we want to inspect every element of a list, and drop those
that do not suit some criterion.
How should we write the type signature of a function that checks
an element of the list?
We’re checking some element of type a, and to indicate whether it
passes or fails, we should return a Bool. So our type is:
3
Fancy language for “function returning a Bool.”
22. Functions as parameters
Suppose we want to inspect every element of a list, and drop those
that do not suit some criterion.
How should we write the type signature of a function that checks
an element of the list?
We’re checking some element of type a, and to indicate whether it
passes or fails, we should return a Bool. So our type is:
a −> Bool
How would we pass such a predicate3 as an argument to another
function that will perform the filtering?
3
Fancy language for “function returning a Bool.”
23. The definition of filter
Welcome to the world of higher-order programming!
filter : : ( a −> Bool ) −> [ a ] −> [ a ]
filter [] = []
f i l t e r p ( x : xs )
| p x = x : f i l t e r p xs
| otherwise = f i l t e r p xs
Higher-order programming occurs when we pass around functions
as arguments to other functions. Simple, but amazingly powerful!
24. Turning a polymorphic type into another type
Suppose we want to use the take function on a list of Int values.
How do we figure out what the type of the function will be when
we use it in that context?
Take the type variable a, and substitute our desired type Int
everywhere. So
take : : I n t −> [ a ] −> [ a ]
when applied to a list of Int becomes
take : : I n t −> [ I n t ] −> [ I n t ]
25. Polymorphism and . . . polymorphism
We can do the same rewrite-the-types trick when we want to see
what the type of a polymorphic function would be when used with
another polymorphic type.
What do I mean by this? Consider take for lists-of-lists-of-lists.
Let’s write a polymorphic list as [b], so a polymorphic list of lists
is [[ b ]] .
We’ll thus replace a with [[ b ]] to get this type:
take : : I n t −> [ [ [ b ] ] ] −> [ [ [ b ] ] ]
26. Polymorphism vs type inference (I)
Here’s a definition someone wrote for homework, but they got
confused along the way, since we hadn’t covered types yet:
nth n [] = []
nth n ( x : xs )
| n < 0 = []
| n == 0 = x
| otherwise = n t h ( n−1) x s
Try reasoning about this code.
What was the intended type for the nth function?
What is the type of this version, and why?
27. Polymorphism vs type inference (II)
The original type for nth should have been this:
n t h : : I n t −> [ a ] −> a
But because we didn’t supply a type, and the code wasn’t quite
doing the right thing, the compiler inferred a surprising type:
n t h : : I n t −> [ [ a ] ] −> [ a ]
Notice that its inference was both correct and consistent with what
the code was really doing (but not with what the author initially
thought the code was doing).
28. How does this work in practice?
In real-world code, when you omit type signatures, you place no
constraints on the compiler’s type inference engine.
Therefore, a single type error can lead the inference engine off
in wild logical directions.
If your code passes the typechecker’s scrutiny, you’re not out
of the woods: it may not behave as you expect, since its types
may be different than you thought.
If your code fails to typecheck, the errors are likely to be
confusing, since you gave the compiler no hints about what
you really meant.
29. Polymorphism and type annotations
What does this experience suggest?
Explicit type signatures can actually be useful!
A signature indicates what type we think the code has.
Type inference occurs even when we supply an explicit type.
If our code’s type signature is inconsistent with the type the
compiler infers, we will get a useful compilation error.
30. Real world use of type annotations
Practical coding tips:
Add type signatures to almost all of your top-level definitions.
They’re good for more than the compiler—they’re invaluable
to readers, as documentation of your code’s behaviour!
Let the compiler infer types for almost all local definitions,
i.e. those in let and where clauses.
31. Inferring behaviour from types
In many cases, we can guess what a function probably does, simply
by examining its type (and sometimes its name):
z i p : : [ a ] −> [ b ] −> [ ( a , b ) ]
32. Inferring behaviour from types
In many cases, we can guess what a function probably does, simply
by examining its type (and sometimes its name):
z i p : : [ a ] −> [ b ] −> [ ( a , b ) ]
r e p l i c a t e : : I n t −> a −> [ a ]
33. Inferring behaviour from types
In many cases, we can guess what a function probably does, simply
by examining its type (and sometimes its name):
z i p : : [ a ] −> [ b ] −> [ ( a , b ) ]
r e p l i c a t e : : I n t −> a −> [ a ]
s p l i t A t : : I n t −> [ a ] −> ( [ a ] , [ a ] )
34. What polymorphism tells us
When we see a polymorphic function like this:
myLength : : [ a ] −> I n t
We can make a surprisingly strong statement about its behaviour:
Its behaviour cannot be influenced by either the type or the
values of the elements in the list it receives.
Why is this?
35. What polymorphism tells us
When we see a polymorphic function like this:
myLength : : [ a ] −> I n t
We can make a surprisingly strong statement about its behaviour:
Its behaviour cannot be influenced by either the type or the
values of the elements in the list it receives.
Why is this?
Polymorphism is a way of saying “I cannot know anything
about this type, or inspect or manipulate its values.”
36. What polymorphism tells us
When we see a polymorphic function like this:
myLength : : [ a ] −> I n t
We can make a surprisingly strong statement about its behaviour:
Its behaviour cannot be influenced by either the type or the
values of the elements in the list it receives.
Why is this?
Polymorphism is a way of saying “I cannot know anything
about this type, or inspect or manipulate its values.”
For example, there’s no way to say “myLength should return a
different result for [Char] than for [ Int ].” That’s pretty profound!
37. Homework (I)
Write a function that drops every nth element from a list. It
should return the removed elements in one element of a pair, and
the remainder of the list in another. Provide a type signature for
your function.
every 3 ” abcdefghi ”
== ( ” c f i ” , ” abdegh ” )
>
Write a function that returns the kth least element of a list,
starting from zero as “the least element”.
kminimum 0 [ 3 , 2 , 1 ]
== 1
>
kminimum 2 ” q w e r t y ”
== ’ r ’
>
38. Homework (II)
From inspection of the type and operation of the built-in zipWith
function, write your own implementation.
myZipWith min [ 1 , 2 , 3 ] [ 2 , 2 , 2 , 2 , 2 ]
== [ 1 , 2 , 2 ]
>
Write a function that capitalizes the first letter of every word in a
string. It must preserve whitespace and punctuation.
u c f i r s t ” foo b a r ! ! bAZ”
== ” Foo
> Bar ! ! BAZ”