Haskell The Craft of Funtional Programing
Haskell The Craft of Funtional Programing
Simon Thompson
Preface xiii
vii
viii CONTENTS
21 Conclusion 563
Appendices
B Glossary 579
• Functional languages are used to build secure, error-free, systems that are
used in practice by thousands of people every day. For example, the Xmonad
window manager for linux systems (xmonad.org) is written entirely in Haskell,
and the Cryptol language (www.cryptol.net), used to design cryptographic
systems in C and VHDL, is itself implemented using Haskell.
• Finally, it may well be that you will find yourself working with a functional
language after all, even if it is not Haskell. Microsoft now provide the F# func-
tional language as a standard part of their Visual Studio suite, and this is in
xiii
xiv Preface
addDouble x y = 2*(x+y)
where x and y are the inputs and 2*(x+y) is the result.
The model of functional programming is simple and clean: to work out the value
of an expression like
3 + addDouble 4 5
the equations which define the functions involved in the expression are used, so
3 + addDouble 4 5
; 3 + 2*(4+5)
; 3 + 2*9
; 3 + 18
; 21
This is how a computer would work out the value of the expression, but it is also pos-
sible to do exactly the same calculation using pencil and paper, making transparent
the implementation mechanism.
Preface xv
It is also possible to discuss how the programs behave in general. In the case of
addDouble we can use the fact that x+y and y+x are equal for all numbers x and
y to conclude that addDouble x y and addDouble y x are equal for all x and y.
A property like this can be tested against random data, or indeed we can formally
prove something like this from the definition of addDouble. Random testing and
proof for properties like this are much more practical for Haskell than for traditional
imperative and object-oriented (OO) languages.
gramming:
• Types play a prominent role in the text. Every function or object defined has
its type introduced at the same time as its definition. Not only does this pro-
vide a check that the definition has the type that its author intended, but also
we view types as the single most important piece of documentation for a def-
inition, since a function’s type describes precisely how the function is to be
used.
• A number of case studies are introduced in stages through the book: the pic-
ture example noted above, the game of Rock - Paper - Scissors, an interac-
tive calculator program, regular expression processing, a coding and decod-
ing system based on Huffman codes and a small queue simulation package.
These are used to introduce various new ideas and also to show how existing
techniques work together. There’s an overview of what each case study covers
on page xxi.
Outline
The introduction in Chapter 1 covers the basic concepts of functional programming:
functions and types, expressions and evaluation, definitions, proof and property-
based testing with QuickCheck. Some of the more advanced ideas, such as higher-
order functions and polymorphism, are previewed here from the perspective of the
example of pictures built from characters. A second implementation of pictures
illustrates a discussion about domain-specific languages, which are the subject of
Chapter 19. The picture examples is one of the running examples in the book, which
we revisit a number of times to illustrate new concepts as they are introduced.
Chapter 2 looks at the practicalities of GHCi, the interactive version of the Glas-
gow Haskell Compiler. GHCi comes as a part of the Haskell platform, which is also
introduced here. After looking at the basics of the module system, the standard pre-
lude and the Haskell libraries, we look at a first exercise using an SVG implemen-
tation of the Picture type. These two chapters together cover the foundation on
which to build a course on functional programming in Haskell.
Information on how to build simple programs over numbers, characters, strings
and Booleans is contained in Chapter 3. The basic lessons are backed up with ex-
ercises, as is the case for all chapters from here on. With this basis, Chapter 4 steps
back and examines the various strategies which can be used to define functions,
such as auxiliary functions, local definitions and recursion. This chapter also in-
troduces the simplest data types in the form of enumerated types. These types are
used in the first discussion of the Rock - Paper - Scissors game which is another case
study which we return to later in the book.
Structured data, in the form of tuples, lists and algebraic types come in Chapter
5. Algebraic types are used to represent products and sums, so giving records and
variant records in other terminology. After introducing the idea of lists, program-
ming over lists is performed using two resources: the list comprehension, which ef-
fectively gives the power of map and filter; and the first-order prelude and library
functions.
Nearly all the list prelude functions are polymorphic, and so polymorphism is
introduced at the start of Chapter 6, which also examines the list functions in the
standard prelude, and then uses them in various extended examples, and only in
Chapter 7 is primitive recursion over lists introduced, and a text processing case
study provides a more substantial case study of how recursion is used in defining
list functions.
Chapter 8 shows how simple terminal IO is handled in Haskell: to do this the
xviii Preface
like Haskell, and the distinction between shallow and deep embeddings. Example
DSLs build on earlier examples, as well as introducing new ones such as the genera-
tor language for QuickCheck, which neatly illustrates a monadic DSL.
The text continues with an examination in Chapter 20 of program behaviour, by
which we mean the time taken for a program to compute its result, and the space
used in that calculation. It also explains the basics of how to measure the run-time
behaviour of Haskell programs. Chapter 21 concludes by surveying various appli-
cations and extensions of Haskell as well as looking at further directions for study.
These are backed up with web and other references.
The appendices cover various background topics. The first examines links with
functional and OO programming, and the second gives a glossary of commonly used
terms in functional programming. The others include a summary of Haskell op-
erators and GHCi errors, together with details of the various implementations of
Haskell. The final appendix contains suggestions for larger-scale Haskell projects.
The Haskell code for all the examples in the book, as well as other background
materials, can be downloaded as explained on www.haskellcraft.com.
• A number of new examples have been included, some in a single place and
others running through a number of chapters. These include the Rock - Paper
- Scissors (RPS) game, card games in general, an SVG rendering of Pictures and
regular expression. There is a particular emphasis on using functions as data:
they appear as strategies in RPS and as recognisers for regular expressions.
• One area where Haskell has been particularly successful is in providing the
substrate for developing embedded domain-specific languages (DSLs) and Chap-
ter 19 is devoted to this. It begins by explaining the reasons for developing
DSLs, and then examines the difference between shallow and deep embed-
dings, looking at the examples of pictures and regular expressions. It con-
xx Preface
• The text has been reordered so that some material comes earlier than it did
previously.1 Material on data types comes substantially earlier, with enumer-
ated types coming into Section 4.3 and non-recursive types into Section 5.3.
Programming for IO interactions is introduced in Chapter 8 to support the
Rock - Paper - Scissors example: this treatment simply presents the do no-
tation as the way that IO is programmed, and delays an explanation of the
underlying mechanism to Chapter 18. Finally, local definitions first come into
Section 4.2.
• The second edition used Hugs as its preferred implementation; in this edition
we have moved to using GHCi, which comes as a part of the Haskell Platform.
As well as introducing GHCi, we have added detailed discussions of how to
leverage the best from Haskell libraries and packages through using Hackage,
Cabal, Hoogle and Hayoo!
• The approach to defining functions over lists. To avoid the situation in which
students try to define each new function over lists by recursion, we first intro-
duced list comprehensions and list library functions, only introducing recur-
sion after that. The jury is out over whether it effected the change it was meant
to.
• The introduction of the Pictures case study as a running theme, giving vi-
sual feedback on programs, and also showing the role of higher-order, poly-
morphic functions in general-purpose Haskell list processing. An example I
still like, but seen by some as making Haskell look lame.
1 It is intriguing that requests to move material forward outnumber those to delay it by a factor of
more than ten to one: perhaps the ideal book introduces all its material in the first chapter, and uses the
remaining twenty to discuss and expand on it?
Preface xxi
• The edition was Haskell 98 compliant, and in particular used standard names
for standard functions. It also contained an introduction to the Hugs inter-
preter, which was the implementation of choice for the book
• Using the do notation , rather than the functional notation of >>= and return,
for I/O programs and monadic programs in general.
• A more thorough treatment of Haskell typing and the mechanics of type check-
ing in practice.
Program proof The book emphasises program proof, in a thread starting with Chap-
ter 9, and followed up in Sections 11.6, 14.7, 16.10 and 17.9.
The case studies in the book are designed to illustrate particular points and con-
structs as well as to give examples of larger programs than single function defini-
tions.
Pictures This is used to show the utility of lists in modelling, as well as the value of
the higher-order and polymorphic functions over lists, such as zipWith, map
and (++). Pictures are also used to illustrate shallow and deep embeddings of
domain-specific languages in Chapter 19, with a variant of ‘named’ pictures
giving an example of a monadic DSL.
Rock - Paper - Scissors In this example, we see Move as a first example of an enu-
merated type in Section 4.3, and strategies as an example of ‘functions as data’
in Section 8.1. It also provides an example of an IO interaction later in that
chapter.
Calculator The calculator example begins in Section 14.2 with the Expr type, a re-
cursive algebraic data type describing numerical expressions. Chapter 16 in-
troduces the abstract type of Store used to model the values of the variables.
In Section 17.5 we see how to parse text into numerical expressions and finally
in Section 18.3 we give an interactive read-evaluate-print loop for calculation.
xxii Preface
Library database When this is introduced in Section 5.7 it shows how to build pro-
grams over lists using list comprehensions, without using explicit recursion.
Supermarket billing This example comes in Section 6.7 and again illustrates the
utility of the list library functions independently of explicit recursive defini-
tions.
Text processing In text processing, Section 7.6, we present an example where ex-
plicit recursion over lists is necessary, in contrast to the previous two.
Other examples Other examples of simulation, relations and graphs are used to il-
lustrate lazy evaluation in Chapter 17.
Acknowledgements
A book can only be improved by feedback, and I am very grateful to everyone who
has contributed help and comments. Particular thanks are due to Thomas Schilling,
whose advice on using cabal and hackage, as well as on the wider Haskell landscape,
was absolutely invaluable. Eerke Boiten kindly proofread material on card games,
and Olaf Chitil had a host of practical suggestions from his recent teaching experi-
ence. They and other colleagues from Kent and the USA contributed to lively dis-
cussions on what it means to be a DSL in Haskell, and the role of monads in DSLs.
Colleagues at Erlang Solutions in London were kind enough to provide an au-
dience for the ‘Haskell Evening Class’ where some of this material was used; their
questions and discussions helped to sharpen the presentation in many places.
Rufus Curnow of Addison-Wesley has supported this third edition from its incep-
tion almost to delivery; thanks very much to him for his hard work and patience, and
also to Simon Lake who took over the task in the final stages. The anonymous refer-
ees were focussed and constructive in their advice, both on my suggested changes
and their own.
This book would not exist without all the efforts of those in the Haskell commu-
nity, supporting first-class systems like GHC, and the burgeoning number of open
source packages and applications that make Haskell such a pleasure to use: thanks
very much to all of you!
Much appreciated sabbatical leave from the University of Kent has provided me
with the time to work on this new edition; without this I am sure it would have taken
many more years for the book to come to light.
Finally I thank my family for their support, understanding and encouragement
while I was writing this: I dedicate the book to them.
Preface xxiii
Simon Thompson
Canterbury, December 2010
Chapter 1
Introducing functional
programming
We start with an overview of what functional programming is all about, and what
makes it different from other kinds of programming. In doing this we’ll also begin to
learn some Haskell. The chapter has three aims.
• To see how this all works in practice we’ll introduce a case study of pictures.
We do this not only because it gives us a chance to preview aspects of Haskell
in practice, but also because it’s an example of a domain-specific language
(DSL), for which Haskell is very well suited.
• Finally, we want to give a preview of some of the more powerful and distinc-
tive ideas in functional programming, and contrast them with other program-
ming paradigms like object-oriented programming. We can then show why
functional programming is the approach of choice for many practising pro-
grammers in the financial sector, in Web 2.0 and in efficient multicore pro-
gramming for example. We’ll also explain why a knowledge of functional pro-
gramming will make you a better programmer, whatever area you eventually
work in.
Where it makes sense we give pointers to later chapters of the book where the ideas
are explained in more detail and illustrated by other examples. Many of the topics
we cover are of more general interest and apply to other functional languages, as
1
2 CHAPTER 1. INTRODUCING FUNCTIONAL PROGRAMMING
ues, while an OO programmer will concentrate on the objects, say. Before we can say
anything more about functional programming, we need first of all to explain what it
means to be a function; this we do now.
The function gives an output value which depends upon the input value(s). We will
often also use the term result for the output, and the terms arguments or parame-
ters for the inputs.
A simple example of a function is addition, +, over numbers. Given input values
12 and 34 the corresponding output will be 46.
inputs
output
12
+ 46
34
flipV
where we have illustrated the effect of this reflection on the ‘horse’ image. In a sim-
ilar way we have a function flipH to represent flipping in a horizontal mirror. An-
other function models the inversion of the colours in a (monochrome) image:
invertColour
Some functions will take two arguments, among them a function to scale images,
scale
2
above
1.4. TYPES 5
beside
All the functions here show that functions take a number of inputs (or arguments,
parameters) and produce an output (or result) that depends on those inputs. What’s
more, this result only depends on those inputs, and will always give the same result
for the same inputs.
Before we can fully explain functional programming in Haskell, we have to ex-
plain types, which we do now.
1.4 Types
The functions which we use in functional programs will involve all sorts of different
kinds of value: the addition function + will combine two numbers to give another
number; flipV will transform a picture into a picture; scale will take a picture and
a number and return a picture, and so on.
A type is a collection of values, such as numbers or pictures, grouped together
because although they are different – 2 is not the same as 567 – they are the same
sort of thing, in that we can do the same things to them. In particular, we can apply
the same functions to them. For instance, we can find the larger of any two numbers,
but it doesn’t make sense to do this for two pictures or a number and a picture.
Each of the functions we have looked at so far will accept inputs from particular
types, and give a result of a particular type too. If we look at the addition function,
+, it only makes sense to add two scnumbers but not two pictures, say. This is an
example of the fact that the functions we have been talking about themselves have
a type, and indeed we can illustrate this diagrammatically:
Integer
Integer
Integer +
The diagram indicates that + takes two whole numbers (or Integers) as arguments
and gives an Integer as a result. In a similar way, we can label the scale function
6 CHAPTER 1. INTRODUCING FUNCTIONAL PROGRAMMING
Picture
Picture
Integer scale
to indicate that its first argument is a Picture and its second is an Integer, with its
result being a Picture. We can see an example of this here, where above is correctly
applied to two Pictures
above
but when applied to a Picture and an Integer a type error occurs, indicating that
we have made a mistake:
<interactive>:1:12:
No instance for (Num Picture)
above
arising from the literal `2' at <interactive>:1:12
Possible fix: add an instance declaration for (Num Picture)
In the second argument of `above', namely `2'
We have now explained two of the central ideas in functional programming: a type
is a collection of values, like the whole numbers or integers; a function is an opera-
tion which takes one or more arguments to produce a result. The two concepts are
linked: functions will operate over particular types: a function to scale a picture will
take two arguments, one of type Picture and the other of type Int, and return a
Picture.
In modelling a problem situation – often called a problem domain – types rep-
resent the things1 in the domain, while functions will represent what can be done
1 I have not used the word ‘object’ here because that has a technical meaning in OO languages; you
could use it, but just remember it’s meant in the non-technical sense here.
1.5. THE HASKELL PROGRAMMING LANGUAGE 7
to transform or manipulate the objects. In the case of pictures, we’ll have a type
Picture to represent the pictures themselves, and functions give operations on pic-
tures, such as placing one above another, or scaling a picture.
A rough and ready rule is that types correspond to nouns, while functions, which
transform or combine values from these types, are like verbs. We come back to types
in Section 1.9 below.
http://www.haskell.org/
There are various implementations of Haskell; in this text we shall use GHCi (2010).
‘GHCi’ is an abbreviation for ‘Glasgow Haskell Compiler interactive’: work on the
compiler was started when Simon Peyton Jones and Simon Marlow were at Glasgow
University; they are now both at Microsoft Research in Cambridge. GHCi provides
an excellent environment for the learner, since it is freely available for PC, Linux and
Mac OS X systems, it is efficient and compact and has a flexible user interface.
GHCi is an interpreter – which means loosely that it evaluates expressions step-
by-step as we might on a piece of paper – but it can also load code that has been
compiled into machine language. So, GHCi combines the flexibility of an interpreter
with the efficiency of a compiler, allowing its programs to run with a speed similar to
those written in more conventional languages like C and C++. Details of other differ-
ent implementations of Haskell can be found in Appendix D, and a full description
of all of them is available at the haskell.org page.
GHCi comes as a part of the Haskell Platform (Haskell Platform 2010), a stan-
dard distribution of the compiler and a selection of commonly-used libraries. Other
libraries are available from an extensive online database, HackageDB (Hackage 2010).
The Cabal (Cabal 2010) packaging and distribution infrastructure, which comes as
part of the Haskell Platform, makes it easy to download and install these libraries.
We describe how to work with the interactive version of GHC in the next chapter,
and how to use Hackage and Cabal in Chapter 6.
All the programs and examples used in the text can be downloaded as the Craft3e
package from Cabal and from the website for this book,
http://www.haskellcraft.com/
8 CHAPTER 1. INTRODUCING FUNCTIONAL PROGRAMMING
This site also pulls together a collection of resources, further reading and other back-
ground materials for this text. The site will also keep track of how the book can be
used with later versions of the Haskell Platform.
expression value
(7-3)*2 4*2 8
evaluation
to give the value 8. Expressions are built up by applying functions to numbers and
other expressions built in the same way. In this particular example, the functions
are subtraction - and multiplication *, and the numbers 7, 3 and 2; the value of the
expression is a number. This process of evaluation is automated in an electronic
calculator.
In functional programming we do exactly the same: we evaluate expressions to
give values, but in those expressions we use functions which model our particular
problem. For example, in modelling pictures we will want to evaluate expressions
whose values are pictures. If the picture
is called horse, then we can form an expression by applying the function flipV to
the horse. This function application is written by putting the function followed by
its arguments, like this:
flipV horse
and then evaluation will give
expression value
evaluation
expression
invertColour (flipV )
evaluation
invertColour
value
1.7 Definitions
A functional program in Haskell consists of a number of definitions. A Haskell def-
inition associates a name with a value of a particular type. We often use the word
‘identifier’ instead of ‘name’: they mean exactly the same thing.
In the simplest case a definition will look like this
10 CHAPTER 1. INTRODUCING FUNCTIONAL PROGRAMMING
name :: type
name = expression
as in the example
size :: Integer
size = 12+13
which associates the name on the left-hand side, size, with the value of the expres-
sion on the right-hand side, 25, a value whose type is Integer, the type of whole
numbers or integers. The symbol ‘::’ can be read as “is a / is an”, so the first line
of the last definition reads ‘size is an Integer’. Note also that names for functions
and other values begin with a small letter, while type names begin with a capital
letter.
Suppose that we are supplied with the definitions of horse and the various func-
tions over Picture mentioned earlier – we will discuss in detail how to download
these and use them in a program in Chapter 2 – we can then write definitions which
use these operations over pictures. For example, we can say
blackHorse :: Picture
blackHorse = invertColour horse
so that the Picture associated with blackHorse is obtained by applying the func-
tion invertColour to the horse, thus giving
flipV
flipH
Integer Integer
n square n*n
The first line of the Haskell definition of square declares the type of the thing being
defined. The arrow -> signifies that this is a function, with one input, an Integer,
appearing before the arrow, and, coming after the arrow, a result of type Integer.
So, we can read square :: Integer -> Integer as
The second line gives the definition of the function: the equation says that when
square is applied to an unknown or variable n, then the result is n*n. How should
we read an equation like this? Because n is an arbitrary, or unknown value, it means
that the equation holds whatever the value of n, so that it will hold whatever integer
expression we put in the place of n, so that, for instance
square 5 = 5*5
and
This is the way that the equation is used in evaluating an expression which uses
square. If we need to evaluate square applied to the expression e, we replace the
application square e with the corresponding right-hand side, e*e.
In general a simple function definition will take the form
name x1 x2 ... xk = e
The variables used in an equation defining a function stand for arbitrary values: the
definition holds for whatever value is chosen for these inputs. These variables are
called the formal parameters of the function because they stand for arbitrary values
of the parameters: the actual parameters are supplied when the function is applied,
as in square 7, where 7 is the actual parameter to square. We’ll only use ‘formal’
and ‘actual’ in the text when we need to draw a distinction between the two; in most
cases it will be obvious what is meant when ‘parameter’ is used.
Accompanying the definition of the function is a statement or declaration of its
type. That will look like this, using the scale function over pictures as an example:
rotateHorse :: Picture
rotateHorse = rotate horse
which states that rotateHorse is the result of applying the function rotate to the
picture horse.
The pattern of definition of rotate – ‘apply one function, and then apply an-
other to the result’ – is so common that Haskell gives a way of combining functions
directly in this way. We define
flipH.flipV
flipV flipH
we see the creation of a new function by connecting together the input and output
of two given functions: obviously this suggests many other ways of connecting to-
gether functions, many of which we will look at in the chapters to come.
14 CHAPTER 1. INTRODUCING FUNCTIONAL PROGRAMMING
• First, scale has two arguments: the first is a Picture and the second an
Integer; this means that scale can be applied to horse and 3.
• The result of applying scale to this Picture and Integer will be a Picture.
The type thus does two things. First, it expresses a constraint on how the function
scale is applied: it must be applied to a Picture and an Integer. Second, the type
tells us what the result is if the function is correctly applied: in this case the result is
a Picture.
Giving types to functions and other things not only tells us how they can be used;
it is also possible to check automatically that functions are being used in the right
way and this process – which is called type checking – takes place in Haskell. If we
use an expression like
we will be told that we have made an error in applying scale to two pictures when
a picture and a number are what was expected. Moreover, this can be done without
knowing the values of scale or horse – all that we need to know to perform the
check is the types of the things concerned. Thus, type errors like these are caught
before programs are used or expressions are evaluated.
It is remarkable how many errors, due either to mistyping or to misunderstand-
ing the problem, are made by novices and experienced programmers alike. The
Haskell type system therefore helps us to write correct programs, and to avoid a large
proportion of programming pitfalls, both obvious and subtle, typos and misunder-
standings. This is something which you particularly appreciate if you do use other
languages without this kind of type checking, where you need to trace back from a
particular error that happens during evaluation to its source.
1.10. CALCULATION AND EVALUATION 15
Type abstraction
Before moving on, there is another important point which we’ll explore later in the
book. In Sections 1.7 and 1.8 we gave definitions of
blackHorse :: Picture
rotate :: Picture -> Picture
which use the type Picture and some functions already defined over it: flipH and
flipV. We were able to write the definitions of blackHorse and rotate without
knowing anything about how the type of Pictures or the functions working with
Pictures were actually defined. We can use these functions because we know their
types, so we know what they have to be applied to, and what type their results will
be.
Treating the type Picture in this way is called type abstraction: as users of the
type we don’t need to concern ourselves with how the type is defined. The advantage
of this is that the definitions we give apply however pictures are modelled. We might
choose to model them in different ways in different situations; whatever the case,
the function composition flipH . flipV will rotate a picture through 180± . We’ll
see this in practice in Section 1.13 where we give two, very different, models of pic-
tures; Chapter 16 discusses this in more detail, and explains the Haskell mechanism
to support type abstraction.
Now we can replace double (3+1) by 2*(3+1) in (‡), and evaluation can con-
tinue.
One of the distinctive aspects of functional programming is that such a simple
‘calculator’ model is a complete description of computation in Haskell. Because the
model is so straightforward, we can perform evaluations in a step-by-step manner;
in this text we call these step-by-step evaluations calculations. As an example, we
now show the calculation of the expression with which we began the discussion.
16 CHAPTER 1. INTRODUCING FUNCTIONAL PROGRAMMING
23 - (double (3+1))
; 23 - (2*(3+1)) using (dbl)
; 23 - (2*4) arithmetic
; 23 - 8 arithmetic
; 15 arithmetic
where we have used ‘; ’ to indicate a step of the calculation, and on each line we
indicate at the right-hand margin how we have reached that line. For instance, the
second line of the calculation:
says that we have reached here using the definition of the double function, (dbl).
In writing a calculation it is sometimes useful to underline the part of the expres-
sion which gets modified in transition to the next line. This is, as it were, where we
need to focus our attention in reading the calculation. The calculation above will
have underlining added like this:
23 - (double (3+1))
; 23 - (2*(3+1)) using (dbl)
; 23 - (2*4) arithmetic
; 23 - 8 arithmetic
; 15 arithmetic
In what is to come, when we introduce a new feature of Haskell we shall show how
it fits into this line-by-line model of evaluation. This has the advantage that we can
then explore new ideas by writing down calculations which involve these ideas.
If you know Java, or another OO or imperative language like C, C++ or C#, then
you need to understand that Haskell variables are very different from variables in
these languages. A Java variable is like a box, where values can be stored: the value is
changed by making an assignment. In Java we compute by changing these contents,
or thestate as it is called. Methods which change state are said to have side-effects.
By contrast, Haskell variables don’t vary, and the way we program is to write
functions which describe how particular data values are related. These functions
don’t have side-effects, and there is no state in Haskell. We’ll find about how all of
this works in the chapters to come, we can summarize the important advantages
now.
• Functions in Haskell can themselves be passed around just like any other data.
So, we can use functions as well as all the other Haskell types when we’re mod-
elling complex problems.
• Haskell functions are without side-effects, but in Haskell it’s possible to do I/O,
work with files, and inter-operate with other programming languages. We can
do this using monads which allow these ‘computational effects’ to be embed-
ded inside Haskell and its type system.
• There are often many different ways of solving the same problem, and when
we begin to try to solve a problem it can be very hard to know which approach
to take. Because Haskell programs are free of side-effects it’s much easier to
transform or refactor our programs to have a different design, as we might
need to do before extending or modifying our program.
Taking these together, we can see why Haskell is popular for many tasks, and partic-
ularly for writing domain-specific languages, which we look at now.
SVG pictures
The pictures we’ve seen so far in this chapter are from a model of pictures which can
be displayed in web browsers supporting the SVG standard(SVG 2010), as shown
in Figure 1.2. The web page allows you to view pictures “rendered from” Haskell
descriptions; we’ll come back to the details of how this is done in Section 2.6, page
35. For now the message is that you can use the DSL just knowing the types of the
functions, as they tell you all you need to know to use them: for each function they
tell us the types of the inputs they should be applied to and the type of the result.
Our version of the horse picture, and the same picture flipped in horizontal and
vertical mirrors are shown in Figure 1.3, where we use dots to show the white parts
of the pictures.
How are the pictures built from characters? In our model we think of a picture as
being made up of a list of lines, that is a collection of lines coming one after another
in order. Each line can be seen in a similar way as a list of characters. Because we
often deal with collections of things when programming, lists are built into Haskell.
More specifically, given any type – like characters or lines – Haskell contains a type
of lists of that type, and so in particular we can model pictures as we have already
explained, using lists of characters to represent lines, and lists of lines to represent
pictures.
With this model of Pictures, we can begin to think about how to model func-
tions over pictures. A first definition comes easily; to reflect a picture in a horizontal
mirror each line is unchanged, but the order of the lines is reversed: in other words
we reverse the list of lines:
flipH = reverse
where reverse is a built-in function to reverse the order of items in a list. How do
we reflect a picture in a vertical mirror? The ordering of the lines is not affected, but
instead each line is to be reversed. We can write
since map is the Haskell function which applies a function to each of the items in
a list, individually. In the definitions of flipH and flipV we can begin to see the
power and elegance of functional programming in Haskell.
• We have used reverse to reverse a list of lines in flipH and to reverse each
line in flipV: this is because the same definition of the function reverse can
be used over every type of list. This is an example of polymorphism, or generic
programming, which is examined in detail in Section 6.1.
• In defining flipV we see the function map applied to its argument reverse,
which is itself a function. This makes map a very general function, as it can
have any desired action on the elements of the list, specified by the function
which is its argument. This is the topic of Chapter 10.
• Finally, the result of applying map to reverse is itself a function. This covered
in Chapter 11.
The last two facts show that functions are ‘first-class citizens’ and can be handled
in exactly the same way as any other sort of object like numbers or pictures. The
combination of this with polymorphism means that in a functional language we can
write very general functions like reverse and map, which can be applied in a multi-
tude of different situations.
The examples we have looked at here are not out of the ordinary. We can see that
other functions over pictures have similarly simple definitions. We place one picture
22 CHAPTER 1. INTRODUCING FUNCTIONAL PROGRAMMING
above another simply by joining together the two lists of lines to make one list. This
is done by the built-in operator ++, which joins together two lists:2
above = (++)
To place two pictures beside each other we have to join corresponding lines
together, thus
.......##... ++ ......##....
.....##..#.. ++ .....#.#....
...##.....#. ++ ....#..#....
..#.......#. ++ ...#...#....
..#...#...#. ++ ..#...#.....
..#...###.#. ++ .#....#..##.
.#....#..##. ++ ..#...###.#.
..#...#..... ++ ..#...#...#.
...#...#.... ++ ..#.......#.
....#..#.... ++ ...##.....#.
.....#.#.... ++ .....##..#..
......##.... ++ .......##...
and this is defined using the function zipWith. This function is defined to ‘zip to-
gether’ corresponding elements of two lists using – in this case – the operator ++.
Given the picture library, we can also look at how the functions work together, and
a simple way of doing this is to check how a combination of applications works. For
example,
2 The operator ++ is surrounded by parentheses (...) in this definition so that it is interpreted as a
function; we say more about this in Section 3.7.
1.14. TESTS, PROPERTIES AND PROOFS 23
flipV
flipH
flipH
flipV
• if we flip a picture twice in a mirror we should get back the original picture;
These tests can be defined in Haskell, where the equality operator ‘==’ is used to
check whether two values are equal, returning the result True or False. These two
values are the two elements of the Boolean type, Bool, which we come back to in
the next chapter. Here are the tests:
Coverage
Property-based testing has replaced one test with a hundred, but we might still be
unlucky, and miss the failing cases in the random data. On the other hand, having a
proof gives us complete certainty that a function is correct.
1.14. TESTS, PROPERTIES AND PROOFS 25
Figure 1.5 illustrates the coverage given by the different mechanisms. Testing
will check how the program behaves at a few, well-chosen, points; property-based
testing expands this to hundreds of randomly-generated points, which, it is hoped,
are representative, but a point where an error occurs may be missed3 ; proof covers
all cases, no exceptions.
Proof
A proof is a logical or mathematical argument to show that something holds in all
circumstances. For example, given any particular right-angled triangle
a
c
we can check whether or not a2 =b2 +c2 holds. In each case we check, this formula
will hold, but this is not in itself enough to show that the formula holds for all a, b
and c. A proof of Pythagoras’s Theorem, on the other hand, is a general argument
which establishes that a2 =b2 +c2 holds whatever right-angled triangle we choose.
How is proof relevant to functional programming? To answer this we go back to
the example of flipping in horizontal and vertical mirrors. As we saw in Figure 1.4,
the order of reflection looks as though is not significant, and we can express this as
the property:
• the function flipH leaves each line unaffected, while reversing the order of
the list of lines.
Because the two functions affect different aspects of the list it is immaterial which is
applied first, since the overall effect of applying the two in either case is to
• reverse each line and reverse the order of the list of lines.
Proof is possible for most programming languages, but it is substantially easier for
functional languages than for any other paradigm. Proof of program properties will
be a theme in this text, and we start by exploring proof for list-processing functions
in Chapter 9.
3 We’ll see an example of this in Chapter 9
26 CHAPTER 1. INTRODUCING FUNCTIONAL PROGRAMMING
Summary
As we said at the start, this chapter has three aims. We wanted to introduce some
of the fundamental ideas of functional programming; to illustrate them with the ex-
ample of pictures, and also to give a flavour of what it is that is distinctive about
functional programming. To sum up the definitions we have seen,
• every object has a clearly defined type, and we state this type on making a
definition;
In the remainder of the book we’ll explore different ways of defining new types and
functions, as well as following up the topics of polymorphism, functions as argu-
ments and results, data abstraction and proof which we have touched upon in an in-
formal way here. We’ll also make sure that we validate our programs using property-
based testing in QuickCheck as well as proof. Finally, we’ll see how Haskell is used
in defining domain-specific languages.
Chapter 2
27
28 CHAPTER 2. GETTING STARTED WITH HASKELL AND GHCI
{- #########################################################
FirstScript.hs
Simon Thompson, August 2010.
######################################################### -}
size :: Integer
size = 12+13
example :: Integer
example = double (size - square (2+2))
http://hackage.haskell.org/platform/
http://hackage.haskell.org/platform/contents.html
Further information about downloading and installing the Haskell Platform may be
found in Appendix D; we discuss how to find out more about the functions and li-
braries in the Haskell Platform in Chapter 6.
GHCi documentation
On Mac OS X documentation for GHCi and other systems is found at
30 CHAPTER 2. GETTING STARTED WITH HASKELL AND GHCI
file:///usr/share/doc/ghc/html/index.html
Linux systems differ: for example, in Ubuntu 9.10 the path to the Haskell documen-
tation is:
/usr/share/doc/ghc6-doc
Links to the documentation are found in the Haskell Platform program group on
Windows systems.
Starting GHCi
To start GCHi on OS X and Linux, type ghci to the prompt; to launch GHCi using a
particular file, type ghci followed by the name of the file in question, as in
ghci Chapter2
Haskell scripts carry the extension .hs; only such files can be loaded, and their
extensions can be omitted when they are loaded either when GHCi is launched or
by a :load command within GHCi.
where we have indicated the machine output by using a slanted font; user input
appears in unslanted form. The prompt here, *Chapter2> , will be explained in
Section 2.5 below. As can be seen from the examples, we can evaluate expressions
which use the definitions in the current script. In this case it is Chapter2.hs.
One of the advantages of the GHCi interface is that it is easy to experiment with
functions, trying different evaluations simply by typing the expressions at the key-
board. If we want to evaluate a complex expression, we could add it to the program,
as in definition
but we can also input these definitions directly into GHCi by prefacing them with
the keyword let, as seen in Figure 2.2. Of course, this definition will be lost when
we close GHCi, so if we want to keep a record of it, then we should add it to a Haskell
module as well.
GHCi commands
GHCi commands begin with a colon, ‘:’. A summary of the main commands is given
in Figure 2.4. When a GHCi command can be abbreviated to their initial letter this
is shown in the table in Figure 2.4. Outline information about other commands is
given by the :help command, and comprehensive details can be found in the on-
line GHCi documentation discussed above.
Editing scripts
GHCi can be connected to a default text editor, so that GHCi commands such as
:edit use this editor. This may well be determined by your local set-up (e.g. in the
EDITOR variable), or can be set using the :set command in GHCi.
Using the GHCi :edit command causes the editor to be invoked on the appro-
priate file. When the editor is quit, the updated file is loaded automatically. However,
it can be more convenient to keep the editor running in a separate window and to
reload the file by:
32 CHAPTER 2. GETTING STARTED WITH HASKELL AND GHCI
:load Parrot :l Load the Haskell module Parrot.hs; the file ex-
tension .hs can be omitted.
:reload :r Repeat the last :load command.
:type exp :t Give the type of the expression exp; e.g. typing
:type size+2 gives size+2 :: Integer.
:info name :i Give information about the thing called name.
:browse Name Give information about the definitions in the mod-
ule Name, if it is loaded.
:quit :q Quit the system.
:help :h,:? Give a complete list of the GHCi commands.
:! command Escape to perform a Unix or DOS command.
:edit First.hs :e Edit the file First.hs in the default editor. Note
that the file extension .hs is needed in this case.
See the following section for more information on
editing.
:set editor vi :s Set the editor to be vi.
", # Move up (") and down (#) the command history.
!| Name and command completion: complete mod-
ule or file names, or GHCi commands.
let s = exp Give s the value of exp within this GHCi session.
• writing the updated file from the editor (without quitting it), and then
In this way the editor is still open on the file should it need further modification.
Alternatively some editors allow GHCi to be called from within the editor, as a
part of the Haskell mode for the editor. A popular Haskell editor is emacs, with vari-
ants such as Aquamacs for Mac OS X. These editors will provide facilities such as
syntax highlighting, and embedded evaluation. In Haskell mode for emacs, ˆC ˆB
opens GHCi and ˆC ˆL (re)loads the current module in GHCi.
Task 1
Load the file FirstScript.hs into GHCi, and evaluate the following expressions
square size
square
double (square 2)
2.4. THE STANDARD PRELUDE AND THE HASKELL LIBRARIES 33
it
square (double 2)
let d = double 2
square d
23 - double (3+1)
23 - double 3+1
it + 34
13 ‘div‘ 5
13 ‘mod‘ 5
On the basis of this can you work out the purpose of it and let?
Task 2
Use the GHCi command :type to tell you the type of each of these, apart from it.
Task 3
double 2 3
double square
2 double
Task 4
• The function should double its input and square the result of that.
• The function should square its input and double the result of that.
:browse Prelude
34 CHAPTER 2. GETTING STARTED WITH HASKELL AND GHCI
let in GHCi
It is possible to make temporary definitions in GHCi using let like this:
let s = 23
and once you have done this you can use s in any expressions you evaluate. You can
define any Haskell value this way, too.
Beware! These definitions are lost if you redefine s or when you leave GHCi, so it
often best to put definitions into a file, so that they are saved and you can edit them
subsequently.
Module
Module
Libraries Module Prelude
expression
User GHCi
result
which will list the types of all the functions in the Prelude.hs module.
As Haskell has developed over the last decade, the prelude has also grown. In or-
der to make the prelude smaller, and to free up some of the names used in it, many
of the definitions have been moved into standard libraries, which can be included
when they are needed. We shall say more about these libraries as we discuss partic-
ular parts of the language.
As well as the standard libraries, the Haskell Platform includes various contributed
libraries which support concurrency, functional animations and so forth. Again, we
will mention these as we go along. Other libraries are available using the Cabal in-
stallation system: see Chapter 6 for details. In order to use the libraries we need to
know something about Haskell modules, which we turn to now.
2.5 Modules
A typical piece of computer software will contain thousands of lines of program text.
To make this manageable, we need to split it into smaller components, which we call
2.6. A SECOND EXAMPLE: PICTURES 35
modules.
A module has a name and will contain a collection of Haskell definitions. To
introduce a module called Ant we begin the program text in the file thus:
A module may also import definitions from other modules. The module Bee will
import the definitions in Ant by including an import statement, thus:
The import statement means that we can use all the definitions in Ant when making
definitions in Bee. In dealing with modules in this text we adopt the conventions
that
The module mechanism supports the libraries we discussed in Section 2.4, but we
can also use it to include code written by ourselves or someone else.
The module mechanism allows us to control how definitions are imported and
also which definitions are made available or exported by a module for use by other
modules. We look at this in more depth in Chapter 15, where we also ask how mod-
ules are best used to support the design of software systems.
In the light of what we have seen so far, Figure 2.5 illustrates a GHCi session. like
this: The current module will have access to the standard prelude, and to those mod-
ules which it imports; these might include modules from the standard libraries,
which are found in the same directory as the standard prelude. The user interacts
with GHCi, providing expressions to evaluate and other commands and receiving
the results of the evaluations.
The next section revisits the picture example of Chapter 1, which is used to give
a practical illustration of modules.
ghci PicturesSVG.hs
36 CHAPTER 2. GETTING STARTED WITH HASKELL AND GHCI
and in your web browser of choice2 open the file refresh.html in the same
directory.
This will give a browser display as shown in Figure 1.2, page 19. The image in
the browser will update automatically; if you would prefer to do this manually,
use the file showPic.html instead.
ghci Pictures.hs
.......##...
.....##..#..
...##.....#.
..#.......#.
..#...#...#.
..#...###.#.
.#....#..##.
..#...#.....
...#...#....
....#..#....
.....#.#....
......##....
first seen in Chapter 1. Any Picture can be printed in a similar way. The
Pictures module is shown in Figure 2.6.
2 Both Firefox and Google Chrome work with SVG on all platforms; Safari on Mac OS X works apart
from invertColour. Internet Explorer 8 doesn’t support SVG, but IE9 is predicted to.
38 CHAPTER 2. GETTING STARTED WITH HASKELL AND GHCI
Exercises
2.1 Define a module UsePictures which imports Pictures (or PicturesSVG)
and contains definitions of blackHorse and rotateHorse which can use the
definitions imported from the pictures module.
In the remaining questions you are expected to add other definitions to your
module UsePictures.
2.2 How could you make the picture
Try to find two different ways of getting the result. It may help to work with
pieces of white and black paper.
Using your answer to the first part of this question, how would you define a
chess (or checkers) board, which is an 8 £ 8 board of alternating squares?
2.3 Three variants of the last picture which involve the ‘horse’ pictures are
We will explore the technical details behind these messages in a later chapter; for
now it is sufficient to read these as ‘Type Error!’. One thing we can focis on, though, is
the place that it says the error occurs: suppose that this was inside a larger program,
it would still indicate the appearance of 4 double as giving rise to the problem. So,
always take note of where an error is said to occur.
The last kind of error we will see are program errors. Try the expression
4 ‘div‘ (3*2-6)
We cannot divide by zero (what would the result be?) and so we get the message
indicating that a division of 4 by 0 has occurred. More details about the error mes-
sages produced by GHCi can be found in Appendix E.
Summary
The main aim of this chapter is practical, to acquaint the reader with the GHCi im-
plementation of Haskell. We have seen how to write simple Haskell programs, to
load them into GHCi and then to evaluate expressions which use the definitions in
the module.
Larger Haskell programs are structured into modules, which can be imported
into other modules. Modules support the Haskell library mechanism and we illus-
trate modules in the case study of Pictures introduced in Chapter 1.
We concluded the chapter with an overview of the possible syntax, type and pro-
gram errors in expressions or scripts submitted to GHCi.
The first two chapters have laid down the theoretical and practical foundations
for the rest of the book, which explores the many aspects of functional programming
using Haskell and GCHi.
Chapter 3
We have now covered the basics of functional programming and have shown how
simple programs are written, modified and run in GHCi. This chapter covers Haskell’s
most important basic types and also shows how to write definitions of functions
which have multiple cases to cover alternative situations. We conclude by looking at
some of the details of the syntax of Haskell.
Haskell contains a variety of numerical types. We have already seen the Integer
type in use; we shall cover this as well as the (related) Int type and the floating-point
fractional numbers, Float.
Often in programming we want to make a choice of values, according to whether
or not a particular condition holds. These conditions include tests of whether one
number is greater than another; whether two values are equal, and so on. The re-
sults of these tests – True if the condition holds and False if it fails – are called the
Boolean values, after the nineteenth-century logician George Boole, and they form
the Haskell type Bool. In this chapter we cover the Booleans, and how they are used
to give choices in function definitions by means of guards.
Next, we look at the types of characters and strings. Characters – individual let-
ters, digits, spaces and so forth – are given by the Haskell type Char. Strings of letters
and other characters make up strings, in the Haskell type String.
The chapter provides reference material for the basic types; a reader may skip
the treatment of Float and much of the detail about Char and String, referring
back to this chapter when necessary.
Each section here contains examples of functions, and the exercises build on
these. Looking ahead, this chapter gives a foundation on top of which we look at
a variety of different ways that programs can be designed and written, which is the
topic of the next chapter.
41
42 CHAPTER 3. BASIC TYPES AND DEFINITIONS
than the second. The Boolean type in Haskell is called Bool. The Boolean operators
provided in the language are:
&& and
|| or
not not
Because Bool contains only two values, we can define the meaning of Boolean oper-
ators by truth tables which show the result of applying the operator to each possible
combination of arguments. For instance, the third line of the first table says that the
value of False && True is False and that the value of False || True is True.
t1 t2 t1 && t2 t1 || t2 t1 not t1
T T T T T F
T F F T F T
F T F T
F F F F
x
||
&&
&& not
y
Boolean values can also be compared for equality and inequality using the operators
== and /=, which both have the type
The Booleans: Bool 43
Note that /= is the same function as exOr, since both return the result True when
exactly one of their arguments is True.
We can also use a combination of literals and variables on the left-hand side of equa-
tions defining exOr:
Here we see a definition of a function which uses two equations: the first applies
whenever the first argument to exOr is True and the second when that argument is
False.
Definitions which use True and False on the left-hand side of equations are of-
ten more readable than definitions which only have variables on the left-hand side.
This is a simple example of the general pattern matching mechanism in Haskell,
which we look at in detail in Chapter 5.
Testing
We can write some QuickCheck properties to test our new implementation of not
and our multiple implementations of exclusive or. We test myNot against the built in
function, and test our exclusive or functions – let’s call them exOr and exOr1:
prop_myNot x =
not x == myNot x
prop_exOrs x y =
exOr x y == exOr1 x y
prop_exOr2 x y =
exOr x y == (x /= y)
Exercises
3.1 Give another version of the definition of ‘exclusive or’ which works informally
like this: ‘exclusive or of x and y will be True if either x is True and y is False,
or x is False and y is True’.
3.2 Give the ‘box and line’ diagram corresponding to your answer to the previous
question.
3.3 Using literals on the left-hand side we can make the truth table for a function
into its Haskell definition. Complete the following definition of exOr in this
style.
3.4 Give your own definitions of the built-in && and ||. If you want to use the
same operator for &&, say, you will need to make sure you hide its import. You
can do this by adding it to the list of what is hidden, thus:
which returns the result True except when both its arguments are True. Give
a diagram illustrating one of your definitions.
3.7 Write QuickCheck properties to test the functions you have written in the ear-
lier exercises. You might be able to check one version of a function against
another, or perhaps think up different properties for your functions.
0
45
-3452
2147483647
Integers in the Integer type can be as large as you wish: looking back at the GHCi
screenshots in Figures 2.2 and 2.3 you can see examples of this. We do arithmetic on
integers using the following operators and functions.
+ The sum of two integers.
* The product of two integers.
ˆ Raise to the power; 2ˆ3 is 8.
- The difference of two integers, when infix: a-b; the
integer of opposite sign, when prefix: -a.
div Whole number division; for example div 14 3 is 4.
This can also be written 14 ‘div‘ 3.
mod The remainder from whole number division; for ex-
ample mod 14 3 (or 14 ‘mod‘ 3) is 2.
abs The absolute value of an integer; remove the sign.
negate The function to change the sign of an integer.
Note that ‘mod‘ surrounded by backquotes is written between its two arguments,
is an infix version of the function mod. Any function can be made infix in this way.
In what follows we will use the term the natural numbers for the non-negative
integers: 0, 1, 2, . . . .
Relational operators
There are ordering and (in)equality relations over the integers, as there are over all
basic types. These functions take two integers as input and return a Bool, that is
either True or False. The relations are
> greater than (and not equal to)
>= greater than or equal to
== equal to
/= not equal to
<= less than or equal to
< less than (and not equal to)
46 CHAPTER 3. BASIC TYPES AND DEFINITIONS
Negative literals
Negative literals cause problems in Haskell, because of the way that they have been
defined. For example the number minus twelve is written as -12, but the prefix ‘-’
can often get confused with the infix operator to subtract one number from another
and can lead to unforeseen and confusing type error messages. For example, the
application
negate -34
is interpreted as ‘negate minus 34’ and leads to a GHCi error message: we discuss
how to interpret that in the next note.
If you are in any doubt about the source of an error and you are dealing with negative
numbers you should enclose them in parentheses, thus: negate (-34); it will do
no harm! See Section 3.7 for more details.
A simple example using these definitions is a function to test whether three Integers
are equal.
Exercises
3.8 Explain the effect of the function defined here:
Hint: if you find it difficult to answer this question directly, try to see what the
function does on some example inputs.
fourEqual :: Integer -> Integer -> Integer -> Integer -> Bool
3.3. OVERLOADING 47
which returns the value True only if all four of its arguments are equal.
Give a definition of fourEqual modelled on the definition of threeEqual
above. Now give a definition of fourEqual which uses the function threeEqual
in its definition. Compare your two answers.
3.12 Devise QuickCheck tests for the functions that you have defined here.
3.3 Overloading
Integers, Ints and Booleans can all compared for equality, and the same symbol
== is used for all these operations, even though they are different. Indeed, == will be
used for equality over any type t for which we are able to define an equality operator.
This means that (==) will have the type
2 == True
we get an error message from GHCi. Of course, there’s not much point in comparing
values of different types, because they will never be equal! We’ll come back to this in
Chapter 13.
3.4 Guards
Here we explore how conditions or guards are used to give alternatives in the defini-
tions of functions. A guard is a Boolean expression, and these expressions are used
to express various cases in the definition of a function.
We take as a running example in this section functions which compare integers
for size, and start by looking at the example of the function to return the maximum
value of two integers. When the two numbers are the same then we call their com-
3.4. GUARDS 49
In general, if the first guard (here x>=y) is True then the corresponding value is the
result (x in this case). On the other hand, if the first guard is False, then we look at
the second, and so on. An otherwise guard will hold whatever the arguments, so
that in the case of max the result is x if x>=y and y otherwise, that is in the case that
y>x.
An example in which there are multiple guards is a definition of the maximum
of three inputs.
y >= z
If this holds, the result is y; otherwise the result is z. We will go back to the example
of maxThree in Section 4.1.
We first gave a general form for simple function definitions in Chapter 1; we can
now strengthen this to give a general form for function definitions with guards in
Figure 3.1. Note that the otherwise is not compulsory.
We also saw in Chapter 1 that we can write down line-by-line calculations of the
values of expressions. How do guards fit into this model? When we apply a function
50 CHAPTER 3. BASIC TYPES AND DEFINITIONS
name x1 x2 ... xk
Guards | g1 = e1 Results
| g2 = e2
...
| otherwise = e
Figure 3.1: The general form for function definitions with guards.
to its arguments we need to know which of the cases applies, and to do this we need
to evaluate the guards until we find a guard whose value is True; once we find this,
we can evaluate the corresponding result. Taking the example of maxThree, we give
two examples in which we perform the evaluation of guards on lines beginning ‘??’.
maxThree 4 3 2
?? 4>=3 && 4>=2
?? ; True && True
?? ; True
; 4
In this example the first guard we try, 4>=3 && 4>=2, gives True and so the result is
the corresponding value, 4. In the second example we have to evaluate more than
one guard.
maxThree 6 (4+3) 5
?? 6>=(4+3) && 6>=5
?? ; 6>=7 && 6>=5
?? ; False && True
?? ; False
?? 7>=5
?? ; True
; 7
In this example we first evaluate the first guard, 6>=(4+3) && 6>=5, which results
in False; we therefore evaluate the second guard, 7>=5, which gives True, and so
the result is 7.
Once we have calculated the value of the second argument, (4+3), we do not
re-calculate its value when we look at it again. This is not just a trick on our part;
the GHCi system will only evaluate an argument like (4+3) once, keeping its value
in case it is needed again, as indeed it is in this calculation. This is one aspect of lazy
evaluation, which is the topic of Chapter 17.
3.4. GUARDS 51
Conditional expressions
Guards are conditions which distinguish between different cases in definitions
of functions. We can also write general conditional expressions by means of the
if. . . then. . . else construct of Haskell. The value of
if condition then m else n
is m if the condition is True and is n if the condition is False, so that the expres-
sion if False then 3 else 4 has the value 4, and in general
Testing
We can test our implementations of ‘maximum’ by checking that they have the same
behaviour, as we did earlier, writing a QuickCheck property like this:
• The maximum of x and y will actually be equal to one (or both) of x and y.
prop_max1 x y =
x <= max x y && y <= max x y
prop_max2 x y =
x == max x y || y == max x y
and check whether they hold.
52 CHAPTER 3. BASIC TYPES AND DEFINITIONS
prop_max3 x y =
(x == max x y) ‘exOr‘ (y == max x y)
recall that QuickCheck will give us a counterexample, that is, an example of where
the test fails:
Exercises
3.15 Define QuickCheck properties to test the functions maxThree, min and minThree.
appears in a script then this definition will conflict with the prelude definition, lead-
ing to a GHCi error messages like this
To redefine the prelude functions max and min, say, the line
Characters: Char
Literal characters are written inside single quotes, thus ’d’ is the Haskell represen-
tative of the character d. Similarly ’3’ is the character three. Some special charac-
ters are represented as follows
tab ’\t’
newline ’\n’
backslash (\) ’\\’
single quote (’) ’\’’
double quote (") ’\"’
There is a standard coding for characters as integers, called the ASCII coding. The
capital letters ’A’ to ’Z’ have the sequence of codes from 65 to 90, and the small
letters ’a’ to ’z’ the codes 97 to 122. The character with code 34, for example, can
be written ’\34’, and ’9’ and ’\57’ have the same meaning. ASCII has recently
been extended to the Unicode standard, which contains characters from other char-
acter sets than English.
There are conversion functions between characters and their numerical codes
which convert an integer into a character, and vice versa.
The coding functions can be used in defining functions over Char. To convert a
54 CHAPTER 3. BASIC TYPES AND DEFINITIONS
offset :: Int
offset = fromEnum ’A’ - fromEnum ’a’
Note that the offset is named, rather than appearing as a part of toUpper, as in
This is standard practice, making the program both easier to read and to modify. To
change the offset value, we just need to change the definition of offset, rather than
having to change the function (or functions) which use it.
Characters can be compared using the ordering given by their codes. So, since
the digits 0 to 9 occupy a block of adjacent codes 48 to 57, we can check whether a
character is a digit thus:
Exercises
3.16 Define a function to convert small letters to capitals which returns unchanged
characters which are not small letters.
which converts a digit like ’8’ to its value, 8. The value of non-digits should
be taken to be 0.
Strings: String
The String type is made up of sequences of characters, written between double
quotes like this: "This is a string!". In the last section we showed how to write
the special characters such as newline and tab using the ‘escapes’ ’\n’ and ’\t’.
These characters can form part of strings, as in the examples
"baboon"
""
"\99a\116"
"gorilla\nhippo\nibex"
"1\t23\t456"
The characters: Char 55
If we evaluate one of these strings in GHCi, the result is exactly the same as the input.
In order to resolve the escape characters and to lose the double quotes we have to
perform an output operation. This is done using the primitive Haskell function
baboon
cat
gorilla
hippo
ibex
1 23 456
Strings can be joined together using ++, so that "cat"++"\n"++"fish" prints as
cat
fish
We’ll cover strings in more detail – in particular seeing how we can write func-
tions to create and manipulate strings – in Chapter 5 below.
In some situations it will not be clear what should be the result type for read – it is
then possible to give a type to the application, as in
the result of which will be 3 and its type, Int. A full explanation of the types of read
and show can be found in Chapter 13.
Exercises
which takes three strings and returns a single string which when printed shows
the three strings on separate lines.
0.31426
-23.12
567.347
4523.0
Floating-point numbers: Float 57
The numbers are called floating point because the position of the decimal point is
not the same for all Floats; depending upon the particular number, more of the
space can be used to store the integer or the fractional part.
Haskell also allows literal floating-point numbers in scientific notation. These
take the form below, where their values are given in the right-hand column of the
table
231.61e7 231.61 £ 107 = 2, 316, 100, 000
231.6e-2 231.61 £ 10°2 = 2.3161
-3.412e03 °3.412 £ 103 = °3412
This representation is more convenient than the decimal numerals above for very
large and small numbers. Consider the number 2.1444 . This will need well over a
hundred digits before the decimal point, and this would not be possible in decimal
notation of limited size (usually 20 digits at most). In scientific notation, it will be
58 CHAPTER 3. BASIC TYPES AND DEFINITIONS
Non-numerical results
Some calculations over floating point numbers don’t give numerical results. These
could be signalled in a variety of ways; Haskell will return an indication that the re-
sult is ‘not a number’, NaN, or infinite, Infinity. These give an indication of what
has gone wrong, but can’t be used in further calculations: once a value is ‘not a num-
ber’ any calculation with it will have the same result.
where fromIntegral takes anything of integral type (that is Int, Integer etc.) to
the corresponding Float.
written as 1.162433e+143.
Haskell provides a range of operators and functions over Float in the standard pre-
lude. The table in Figure 3.2 gives their name, type and a brief description of their
behaviour. Included are the
• standard mathematical operations: square root, exponential, logarithm and
trigonometric functions;
• functions to convert integers to floating-point numbers: fromInt, and vice
versa: ceiling, floor and round.
Haskell can be used as a numeric calculator. Try typing the expression which follows
to the GHCi prompt:
sin (pi/4) * sqrt 2
and the relational operators == and so forth are available over all basic types. We
shall explore this idea of overloading in more detail when we discuss type classes
below in Chapter 13.
Exercises
which returns how many of its inputs are larger than their average value.
3.21 How would you write QuickCheck properties to test the functions averageThree
and howManyAboveAverage?
This assumes that a is non-zero — the case which we call non-degenerate. In the
degenerate case, there are three sub-cases:
Exercises
that given the coefficients of the quadratic, a, b and c, will return how many
roots the equation has. You may assume that the equation is non-degenerate.
that given the coefficients of the quadratic, a, b and c, will return how many
roots the equation has. In the case that the equation has every number a root
you should return the result 3.
which return the smaller and larger real roots of the quadratic. In the case that
the equation has no real roots or has all values as roots you should return zero
as the result of each of the functions.
3.25 How would you write QuickCheck properties to test the functions smallerRoot
and largerRoot?
Hint: one thing you would expect is that the result of the first function is
less than or equal to the second. Another thing you should expect is that
if you substitute the roots back into the equation, the result should be zero.
However, because floating-point calculation is only approximate, you need to
check whether the result of substituting a root is close to zero, rather than be-
ing actually equal to it.
3.7 Syntax
The syntax of a language describes all the properly formed programs. This section
looks at various aspects of the syntax of Haskell, and stresses especially those which
might seem unusual or unfamiliar at first sight.
mystery x = x*x
3.7. SYNTAX 61
mystery x = x*x
+x
+2
. . . until something is found which is on the line or to the left of the line. This closes
the box, like this
mystery x = x*x
+x
+2
next y = ...
In writing a sequence of definitions, it is therefore sensible to give them all the same
level of indentation. In our scripts we shall always write top-level definitions starting
at the left-hand side of the page, and in literate scripts we will indent the start of each
definition by a single ‘tab’.
This rule for layout is called the offside rule because it is reminiscent of the idea
of being ‘offside’ in soccer. The rule also works for conditional equations such as max
and maxThree which consist of more than one clause.
There is, in fact, a mechanism in Haskell for giving an explicit end to part of a
definition, just as ‘.’ does in English: the Haskell ‘end’ symbol is ‘;’. We can, for
instance, use ‘;’ if we wish to write more than one definition on a single line, thus:
answer = 42 ; facSix = 720
Recommended layout
The offside rule permits various different styles of layout. In this book for definitions
of any size we use the form
fun v1 v2 ... vn
| g1 = e1
| g2 = e2
...
| otherwise = er ( or | gr = er )
for a conditional equation built up from a number of clauses. In this layout, each
clause starts on a new line, and the guards and results are lined up. Note also that
by convention in this text we always specify the type of the function being defined.
62 CHAPTER 3. BASIC TYPES AND DEFINITIONS
Layout errors
If we break the offside rule like this:
funny x = x+
1
Chapter3.hs:33:0:
parse error (possibly incorrect indentation)
Failed, modules loaded: none.
which indicates that the indentation may well be the problem. The position 33:0 is
the position of the digit 1, which is in row 33 and column 0 of the file Chapter3.hs.
If any of the expressions ei or guards gi is particularly long, then the guard can
appear on a line (or lines) of its own, like this
fun v1 v2 ... vn
| a long guard which may
go over a number of lines
= very long expression which goes
over a number of lines
| g2 = e2
...
If you use an editor which is Haskell-aware, e.g. emacs with Haskell mode, then the
editor will help you to indent your code. In this particular case, hitting the tab key
repeatedly will cycle through a set of suggested indentations of the line that you are
currently working on, based on its contents.
Names in Haskell
Thus far in the book we have seen a variety of uses of names in definitions and ex-
pressions. In a definition like
the names or identifiers Int, addTwo, first and second are used to name a type,
a function and two variables. Identifiers in Haskell must begin with a letter – small
or capital – which is followed by an optional sequence of letters, digits, underscores
‘_’ and single quotes.
The names used in definitions of values must begin with a small letter, as must
variables and type variables, which are introduced later. On the other hand, capital
letters are used to begin type names, such as Int; constructors, such as True and
3.7. SYNTAX 63
False; module names and also the names of type classes, which we shall encounter
below.
An attempt to give a function a name which begins with a capital letter, such as
Funny x = x+1
Chapter3.hs:32:0:
Not in scope: data constructor ‘Funny’
There are some restrictions on how identifiers can be chosen. There is a small col-
lection of reserved words which cannot be used; these are
The special identifiers as, qualified, and hiding have special meanings in certain
contexts but can be used as ordinary identifiers.
By convention, when we give names built up from more than one word, we cap-
italize the first letters of the second and subsequent words, as in ‘maxThree’.
The same identifier can be used to name both a function and a variable, or both
a type and a type constructor; we recommend strongly that this is not done, as it can
only lead to confusion.
If we want to redefine a name that is already defined in the prelude or one of the
libraries we have to hide that name on import; details of how to do this are given on
page 53.
Haskell is built on top of the Unicode character description standard, which al-
lows symbols from fonts other than those in the ASCII standard. These symbols can
be used in identifiers and the like, and Unicode characters – which are described
by a 16-bit sequence – can be input to Haskell in the form \uhhhh where each of
the h is a hexadecimal (4 bit) digit. In this text we use the ASCII subset of Unicode
exclusively.
Operators
The Haskell language contains various operators, like +, ++ and so on. Operators are
infix functions, so that they are written between their arguments, rather than before
them, as is the case for ordinary functions.
In principle it is possible to write all applications of an operator with enclosing
parentheses, thus
(((4+8)*3)+2)
but expressions rapidly become difficult to read. Instead two extra properties of
operators allow us to write expressions uncluttered by parentheses.
64 CHAPTER 3. BASIC TYPES AND DEFINITIONS
Associativity
If we wish to add the three numbers 4, 8 and 99 we can write either 4+(8+99) or
(4+8)+99. The result is the same whichever we write, a property we call the asso-
ciativity of addition. Because of this, we can write
4+8+99
for the sum, unambiguously. Not every operator is associative, however; what hap-
pens when we write
4-2-1
Binding powers
2ˆ3ˆ2
where the same operator occurs twice, but what is done when two different opera-
tors occur, as in the following expressions?
2+3*4
3ˆ4*2
For this purpose the binding power or fixity of the operators need to be compared.
* has binding power 7 while + has 6, so that in 2+3*4 the 3 sticks to the 4 rather than
the 2, giving
2+3*4 = 2+(3*4)
3ˆ4*2 = (3ˆ4)*2
A full table of the associativities and binding powers of the predefined Haskell op-
erators is given in Appendix C. In the section ‘Do-it-yourself operators’ below we
discuss how operators are defined in scripts and also how their associativity and
binding power can be set or changed by declarations.
3.7. SYNTAX 65
Function application
Binding most tightly is function application, which is given by writing the name of
the function in front of its argument(s) thus: f v1 v2 ... vn . This binds more
tightly than any other operator, so that f n+1 is interpreted as f n plus 1, (f n)+1,
rather than f applied to n+1, f (n+1). If in doubt, it is sensible to parenthesize each
argument to a function application.
Similarly, as ‘-’ is both an infix and a prefix operator, there is scope for confusion.
f -x will be interpreted as x subtracted from f, rather than f applied to -x; the
solution again is to bracket the argument, giving f (-x).
Infix operators can be written before their arguments, by enclosing the operator in
parentheses. We therefore have, for example,
(+) :: Integer -> Integer -> Integer
so that
(+) 2 3 = 2 + 3
This conversion is needed later when we make functions into arguments of other
functions. We can also convert functions into operators by enclosing the function
name in backquotes, thus ‘name‘. We therefore have, using the maximum function
defined earlier,
2 ‘max‘ 3 = max 2 3
This notation can make expressions involving binary or two-argument functions
substantially easier to read.
The fixity and associativity of these operators can be controlled; see Appendix C.
Do-it-yourself operators
The Haskell language allows us to define infix operators directly in exactly the same
way as functions. Operator names are built from the operator symbols which in-
clude the ASCII symbols
! # $ % & * + . / < = > ? \ ˆ | : -
˜
together with the Unicode symbols. An operator name may not begin with a colon.
To define the operator &&& as an integer minimum function, we write
(&&&) :: Integer -> Integer -> Integer
x &&& y
| x > y = y
| otherwise = x
The associativity and binding power of the operator can be specified; for details see
Appendix C.
66 CHAPTER 3. BASIC TYPES AND DEFINITIONS
Exercises
3.26 Rewrite your solutions to the earlier exercises to use the recommended layout.
funny x = x+x
peculiar y = y
explain what happens when you remove the space in front of the peculiar.
Summary
This chapter has introduced the base types Integer, Int, Float, Char and Bool
together with various built-in functions over them. We have seen how Boolean ex-
pressions – called guards – allow definitions which have various cases, and this was
exemplified by the function returning the maximum of two integer arguments. This
definition contains two cases, one which applies when the first argument is the
larger and the other when the second is the larger.
Finally, we have seen how the layout of a Haskell program is significant – the end
of a definition is implicitly given by the first piece of program text ‘offside’ of the start
of the definition; we have also given an overview of operators in Haskell.
This material, together with what we have seen in earlier chapters, gives us a
toolkit which we can use to solve programming problems. In the next chapter we
will explore various ways of using that toolkit to solve practical problems.
Chapter 4
In this chapter we step back from discussing the details of Haskell and instead look at
how to build programs. We present some general strategies for program design; that
is we talk about how programs can be planned before we start to write the details.
The advice we give here is largely independent of Haskell and will be useful whatever
programming language we use.
Two particular aspects of Haskell help with program design, and we introduce
each of these in this chapter. First, we talk about local definitions, which we can use
in solving problems – that is defining functions – step by step. Secondly we start our
discussion of how to define types for ourselves, using Haskell data types.
We follow this by discussing recursion. We begin by concentrating on explaining
why recursion works, and follow this by looking at how to find primitive recursive
definitions, extending what we have said about design. We conclude with an op-
tional examination of more general forms of recursion.
Once we have written a definition we need to ask whether it does what it is in-
tended to do. We conclude the chapter by discussing the principles of program test-
ing and examining a number of examples. We use the unit testing framework HUnit
as well as QuickCheck for presenting and performing tests.
definition 4.1 Design is the stage before we start writing detailed Haskell code.
In this section we will concentrate on looking at examples, and on talking about the
different ways we can try to define functions, but we will also try to give some general
advice about how to start writing a program. These are set out as questions we can
ask ourselves when we are stuck with a programming problem.
67
68 CHAPTER 4. DESIGNING AND WRITING PROGRAMS
• We could say that 2 is the middle number because when we write the numbers
in order: 2 2 4, then 2 is the number that appears in the middle.
• Alternatively we could say that there is no middle number in this case, since 2
is the lower and 4 the higher, and that we therefore cannot return any result.
• First, that even in simple problems there can be things we have to think about
before we start programming.
• Thirdly, a very good way of thinking about whether we understand the prob-
lem is to think about how we expect it to work out in various examples.
• Finally, it is worth realizing that often difficulties like this come out at the pro-
gramming stage, when we have already written a whole lot of definitions; the
sooner we spot a problem like this, the more wasted effort we can save.
Another example of this came up in the definition of max in Section 3.4, where we
had to say what the function should return when its two arguments were the same.
In that case it was sensible to think of the maximum of, say, 3 and 3 as being 3.
In defining maxThree we have the resource of already having defined the function
max. We can think of its definition as a model for how we might define maxThree.
In max we give the result x on condition that it is the maximum of the two, that is
x >= y
Our definition of maxThree does a similar thing, replacing the condition for two
values with the condition for three, namely:
We can use a function we have already defined within the new definition
We are trying to find the maximum of three numbers, and we are already provided
with a function max to give us the maximum of two. How could we use max to give
us the result we want? We can take the maximum of the first two, and then the
maximum of that and the third. In pictures,
x
max
y
max
z
and in Haskell
What if I had any functions I wanted: which could I use in writing the solution?
This what if . . . ? is a central question, because it breaks the problem into two parts.
First we have to give the solution assuming we are given the auxiliary functions we
want and thus without worrying about how they are to be defined. Then, we have
separately to define these auxiliary functions.
Goal Goal
Instead of a single jump from the starting point to the goal, we have two shorter
jumps, each of which should be easier to do. This approach is called top-down as we
start at the top with the overall problem, and work by breaking it down into smaller
problems.
This process can be done repeatedly, so that the overall problem is solved in a
series of small jumps. We now look at an example; more examples appear in the
exercises at the end of the section.
Suppose we are faced with the problem of defining
according to the first of the alternatives described on page 68. A model is given by
the definition of maxThree, in which we give conditions for x to be the solution, y to
be the solution and so on. We can therefore sketch out our solution like this
middleNumber x y z
| condition for x to be solution = x
| condition for y to be solution = y
....
Now, the problem comes in writing down the conditions, but here we say what if we
had a function to do this. Let us call it between. It has three numbers as arguments,
and a Boolean result,
middleNumber x y z
| between y x z = x
| between x y z = y
| otherwise = z
The definition of the function between is left as an exercise for the reader.
This section has introduced some of the general ideas which can help us to get
started in solving a problem. Obviously, because programming is a creative activity
there is not going to be a set of rules which will always lead us mechanically to a so-
lution to a problem. On the other hand, the questions posed here will get us started,
and show us some of the alternative strategies we can use to plan how we are going
to write a program.
We’ll follow this up in the next section, where we look at another way we can
break problems down and solve them in steps. After that we’ll take a first look at
how to define our own data types in solving problems. We take up the discussion
again in Chapter 12.
Exercises
maxFour :: Integer -> Integer -> Integer -> Integer -> Integer
which returns the maximum of four integers. Give three definitions of this
function: the first should be modelled on that of maxThree, the second should
use the function max and the third should use the functions max and maxThree.
For your second and third solutions give diagrams to illustrate your answers.
Discuss the relative merits of the three solutions you have given.
which returns how many of its three arguments are equal, so that
howManyEqual 34 25 36 = 0
howManyEqual 34 25 34 = 2
howManyEqual 34 34 34 = 3
Think about what functions you have already seen – perhaps in the exercises
– which you can use in the solution.
howManyOfFourEqual :: Integer -> Integer -> Integer -> Integer -> Integer
which is the analogue of howManyEqual for four numbers. You may need to
think what if . . . ?.
Examples
4.2. SOLVING A PROBLEM IN STEPS: LOCAL DEFINITIONS 73
fourPics pic =
left ‘beside‘ right
where
left = ...
right = ...
where we define what left and right mean in the local definitions that follow the
where. The reason these are called ‘local’ is that they can only be used in the defini-
tion of the fourPics function, and nowhere else.
So, we have broken the problem down into two parts, each simpler than the
whole problem. Let’s look at the left first. We get this by putting the pic above
an inverted version, so giving us
fourPics pic =
left ‘beside‘ right
where
left = pic ‘above‘ invertColour pic
right = ...
Now, there are a number of ways of finishing off the definition. Let’s look at three of
these now.
fourPics pic =
left ‘beside‘ right
where
left = pic ‘above‘ invertColour pic
right = invertColour (flipV pic) ‘above‘ flipV pic
• We could modify this solution so that we add another local definition to help
us. In this case we define flipped as the original pic flipped in a vertical
mirror. We do this because we can use this in defining the right hand side in a
similar way to how we defined the left.
74 CHAPTER 4. DESIGNING AND WRITING PROGRAMS
fourPics pic =
left ‘beside‘ right
where
left = pic ‘above‘ invertColour pic
right = invertColour flipped ‘above‘ flipped
flipped = flipV pic
• We could use the definition of left in defining right: we get right from
left by reflecting it in a vertical mirror, and then inverting the colour (or the
other way around), so giving the definition
fourPics pic =
left ‘beside‘ right
where
left = pic ‘above‘ invertColour pic
right = invertColour (flipV left)
• Finally, we could define a local function; this is a function we can use only
within the definition of fourPics, and not elsewhere. The function stack
will put a picture above its inverted version; we’ll then use stack in defining
the left and right parts of the picture.
fourPics pic =
left ‘beside‘ right
where
stack p = p ‘above‘ invertColour p
left = stack pic
right = stack (invertColour (flipV pic))
As you can see from this example, there’s often more than one way of solving a prob-
lem, but a very useful tool in each of these solutions was to use local definitions.
Another more mathematical example is given by a function to calculate the area
of a triangle with sides a, b and c. The formula for this is
p
s*(s-a)*(s-b)*(s-c)
triArea a b c
4.2. SOLVING A PROBLEM IN STEPS: LOCAL DEFINITIONS 75
let expressions
It is also possible to make definitions local to an expression, rather than local to a
function clause as for where. For instance, we can write
giving the result 31. If more than one definition is included in one line they need to
be separated by semi-colons, thus:
| possible = sqrt(s*(s-a)*(s-b)*(s-c))
| otherwise = 0
where
s = (a+b+c)/2
possible = ... for you to define ...
Defining possible is left as an exercise: this value should be True only if it’s possible
to have a triangle with those sides. Each of the numbers should be positive, and each
side should satisfy the triangle inequality: the length of the side is less than the sum
of the other two sides.
sumSquares n m
= sqN + sqM
where
sqN = n*n
sqM = m*m
where the where clause is used to find the two squares.
The way in which calculations are written can be extended to deal with where
clauses. The sumSquares function in the previous section gives, for example
sumSquares 4 3
= sqN + sqM
where
sqN = 4*4 = 16
sqM = 3*3 = 9
= 16 + 9
76 CHAPTER 4. DESIGNING AND WRITING PROGRAMS
= 25
The values of the local definitions are calculated beneath the where if their values
are needed. All local evaluation below the where is indented. To follow the top-level
value, we just have to look at the calculation at the left-hand side.
The vertical lines which appear are used to link the successive steps of a calcula-
tion when these have intermediate where calculations. The lines can be omitted.
Layout
In definitions with where clauses, the layout is significant. The offside rule is used
by the system to determine the end of each definition in the where clause.
The where clause must be found in the definition to which it belongs, so that the
where must occur somewhere to the right of the start of the definition. Inside the
where clause, the same rules apply as at the top level: it is therefore important that
the definitions are aligned vertically – if not, an error will result. Our recommended
layout is therefore
f p1 p2 ... pk
| g1 = e1
...
| otherwise = er
where
v1 a1 ... an = r1
v2 = r2
....
The where clause here is attached to the whole of the conditional equation, and so
is attached to all the clauses of the conditional equation.
This example also shows that the local definitions can include functions – here
v1 is an example of a local function definition. We have given type declarations
for all top-level definitions; it is also possible to give type declarations for where-
defined objects in Haskell. In cases where the type of a locally defined object is not
obvious from its context, our convention is to include a declaration of its type.
Scopes
A Haskell script consists of a sequence of definitions. The scope of a definition is
that part of the program in which the definition can be used. All definitions at the
top-level in Haskell have as their scope the whole script that they are defined in: that
is, they can be used in all the definitions the script contains. In particular they can
be used in definitions which occur before theirs in the script, as in
isOdd n
| n<=0 = False
4.2. SOLVING A PROBLEM IN STEPS: LOCAL DEFINITIONS 77
isEven n
| n<0 = False
| n==0 = True
| otherwise = isOdd (n-1)
Local definitions, given by where clauses, are not intended to be ‘visible’ in the
whole of the script, but rather just in the conditional equation in which they ap-
pear. The same is true of the variables in a function definition: their scope is the
whole of the conditional equation in which they appear.
Specifically, in the example which follows, the scope of the definitions of sqx,
sqy and sq and of the variables x and y is given by the large box; the smaller box
gives the scope of the variable z.
maxsq x y
| sqx > sqy = sqx
| otherwise = sqy
where
sqx = sq x
sqy = sq y
sq :: Int -> Int
sq z = z*z
• the variables appearing on the left-hand side of the function definition – x and
y in this case – can be used in the local definitions; here they are used in sqx
and sqy;
• local definitions can be used before they are defined: sq is used in sqx here;
• local definitions can be used in results and in guards as well as in other local
definitions.
It is possible for a script to have two definitions or variables with the same name. In
the example below, the variable x appears twice. Which definition is in force at each
point? The most local is the one which is used.
maxsq x y
| sq x > sq y = sq x
| otherwise = sq y
where
sq x = x*x
78 CHAPTER 4. DESIGNING AND WRITING PROGRAMS
In the example, we can think of the inner box cutting a hole in the outer, so that the
scope of the outer x will exclude the definition of sq. When one definition is con-
tained inside another the best advice is that different variables and names should be
used for the inner definitions unless there is a very good reason for using the same
name twice.
Finally note that it is not possible to have multiple definitions of the same name
at the same level; one of them needs to be hidden if a clash occurs due to the com-
bination of a number of modules.
Exercises
4.5 Give two other ways of completing the definition of fourPics given in this
section.
4.6 Another way of solving the problem is to break it down into one picture above
another, as in
fourPics pic =
top ‘above‘ bottom
where
top = ...
bottom = ...
which returns the maximum of three integers paired with the number of times
it occurs among the three. A natural solution first finds the maximum, and
then investigates how often it occurs among the three. Discuss how you would
write your solution if you were not allowed to use where-definitions.
maxThreeOccurs 4 5 5
maxThreeOccurs 4 5 4
Suppose that we want to model this in Haskell. One option would be to use integers,
characters or strings (which we’ll find out about later), but none of these is ideal, for
a number of reasons.
• A choice may well be arbitrary: what numbers should we associate with ‘rock’,
which with ‘scissors’?
• The type we would choose would have lots of other elements which don’t cor-
respond to anything in the game.
Whatever the case, we add another line – some ‘Haskell magic’ which we’ll explain
in Chapter 13 – which allows us to compare elements of this type for equality, and
also to be able to see them printed out (or ‘shown’), like so:
Now we can begin to define functions using the Move type. Let’s write a function
which tells us the move to beat a particular move:
and also the move that will lose against a particular move.
In the definition of lose we’ve used a wildcard ‘_’ instead of Scissors in the final
clause of the definition. That is because this clause is only matched when the others
don’t, and that will only happen for the value Scissors.
We will come back to this example in Section 8.1 once we have covered lists in
Haskell, when we’ll look at some of the strategies for playing (and winning!) Rock -
Paper - Scissors.
Exercises
4.11 Define a data type Result which represents the outcome of a round of rock -
paper - scissors, which will either be a win. lose or draw.
so that this gives the outcome of a round for the first player. For example, we
should expect that outcome Rock Scissors should be a win.
4.13 We have added some ‘magic’ to the Chapter4 module to allow QuickCheck
properties to be tested over the Move type. Define a QuickCheck property
which connects the results of beat and lose.
4.14 How would you define a QuickCheck property to test the outcome function?
4.4. RECURSION 81
Standard types
We can see some standard types as being defined in this way. In particular, we could
define Bool like this:
data Bool = False | True
deriving (Show, Eq, Ord)
Deriving Ord means that the elements have an ordering defined on them; in this
case False < True.
Exercises
4.15 Define a type of seasons, Season, and give a function from seasons to temper-
ature given by the type
4.16 Define a type Month and a function from this type to Season, assuming that
you’re in the northern hemisphere.
4.4 Recursion
Recursion is an important programming mechanism, in which a definition of a func-
tion or other object refers to the object itself. This section concentrates on explain-
ing the idea of recursion, and why it makes sense. In particular we give two com-
plementary explanations of how primitive recursion works in defining the factorial
function over the natural numbers. In the section after this we look at how recursion
is used in practice.
but we notice that we are repeating a lot of multiplication in doing this. In working
out
1*2*3*4
we see that we are repeating the multiplication of 1*2*3 before multiplying the re-
sult by 4
1*2*3 *4
and this suggests that we can produce the table in a different way, by saying how to
start
fac 0 = 1 (fac.1)
which starts the table thus
n fac n
0 1
and then by saying how to go from one line to the next
fac n = fac (n-1) * n (fac.2)
since this gives us the lines
n fac n
0 1
1 1*1 = 1
2 1*2 = 2
3 2*3 = 6
4 6*4 = 24
and so on.
What is the moral of this story? We started off describing the table in one way,
but came to see that all we needed was the information in (fac.1) and (fac.2).
• (fac.1) tells us the first line of the table, and
• (fac.2) tells us how to get from one line of the table to the next.
The table is just a written form of the factorial function, so we can see that (fac.1)
and (fac.2) actually describe the function to calculate the factorial, and putting
them together we get
fac :: Integer -> Integer
fac n
| n==0 = 1
| n>0 = fac (n-1) * n
A definition like this is called recursive because we actually use fac in describing
fac itself. Put this way it may sound paradoxical: after all, how can we describe
something in terms of itself? But, the story we have just told shows that the definition
is perfectly sensible, since it gives
4.4. RECURSION 83
• a way of going from the value of fac at a particular point, fac (n-1), to the
value of fac on the next line, namely fac n.
These recursive rules will give a value to fac n whatever the (positive) value n has –
we just have to write out n lines of the table, as it were.
fac 4
; fac 3 * 4
so that (fac.2) replaces one goal – fac 4 – with a simpler goal – finding fac 3 (and
multiplying it by 4). Continuing to use (fac.2), we have
fac 4
; fac 3 * 4
; (fac 2 * 3) * 4
; ((fac 1 * 2) * 3) * 4
; (((fac 0 * 1) * 2) * 3) * 4
Now, we have got down to the simplest case (or base case), which is solved by (fac.1).
; (((1 * 1) * 2) * 3) * 4
; ((1 * 2) * 3) * 4
; (2 * 3) * 4
; 6 * 4
; 24
In the calculation we have worked from the goal back down to the base case, using
the recursion step (fac.2). We can again see that we get the result we want, be-
cause the recursion step takes us from a more complicated case to a simpler one,
and we have given a value for the simplest case (zero, here) which we will eventually
reach.
We have now seen in the case of fac two explanations for why recursion works.
• The bottom-up explanation says that the fac equations can be seen to gener-
ate the values of fac one-by-one from the base case at zero.
84 CHAPTER 4. DESIGNING AND WRITING PROGRAMS
• A top-down view starts with a goal to be evaluated, and shows how the equa-
tions simplify this until we hit the base case.
The two views here are related, since we can think of the top-down explanation gen-
erating a table too, but in this case the table is generated as it is needed. Starting
with the goal of fac 4 we require the lines for 0 to 3 also.
Technically, we call the form of recursion we have seen here primitive recursion.
We will describe it more formally in the next section, where we examine how to start
to find recursive definitions. Before we do that, we discuss another aspect of the fac
function as defined here.
because fac is not defined on the negative numbers, since the patterns in the defi-
nition of fac don’t cover the case of negative numbers.
We could if we wished extend the definition to zero, on the negative numbers,
thus
fac n
| n==0 = 1
| n>0 = fac (n-1) * n
| otherwise = 0
fac n
| n==0 = 1
| n>0 = fac (n-1) * n
| otherwise = error "fac only defined on natural numbers"
Exercises
4.17 Define the function rangeProduct which when given natural numbers m and
n returns the product
m*(m+1)*...*(n-1)*n
4.5. PRIMITIVE RECURSION IN PRACTICE 85
You should include in your definition the type of the function, and your func-
tion should return 0 when n is smaller than m.
Hint: you do not need to use recursion in your definition, but you may if you
wish.
4.18 As fac is a special case of rangeProduct, write a definition of fac which uses
rangeProduct.
fun n
| n==0 = .... (prim)
| n>0 = .... fun (n-1) ....
What if we were given the value fun (n-1). How could we define fun n from it?
We see how this form of recursion works in practice by looking at some examples.
Example
1. Suppose first that we are asked to define the function to give us powers of two
for natural numbers
power2 n
| n==0 = ....
| n>0 = .... power2 (n-1) ....
In the zero case the result is 1, and in general 2n is 2n-1 multiplied by 2, so we define
power2 n
| n==0 = 1
| n>0 = 2 * power2 (n-1)
86 CHAPTER 4. DESIGNING AND WRITING PROGRAMS
If we are told that sumFacs 4 is 34 then we can work out sumFacs 5 in one step: we
simply add fac 5, that is 120, giving the result 154. This works in general, and so
we can fill in the template like this:
sumFun fac 3
; sumFun fac 2 + fac 3
; sumFun fac 1 + fac 2 + fac 3
; sumFun fac 0 + fac 1 + fac 2 + fac 3
; fac 0 + fac 1 + fac 2 + fac 3
; ...
; 10
and we can define sumFacs from sumFun thus:
We will get the most new regions if we cross each of these lines; because they are
straight lines, we can only cut each one once. This means that the new line crosses
exactly n of the regions, and so splits each of these into two. We therefore get n
new regions by adding the nth line. Our function definition is given by filling in the
template (prim) according to what we have said.
Exercises
4.19 Using the addition function over the natural numbers, give a recursive defini-
tion of multiplication of natural numbers.
4.20 The integer square root of a positive integer n is the largest integer whose
square is less than or equal to n. For instance, the integer square roots of 15
and 16 are 3 and 4, respectively. Give a primitive recursive definition of this
function.
4.21 Given a function f of type Integer -> Integer give a recursive definition of
a function of type Integer -> Integer which on input n returns the maxi-
mum of the values f 0, f 1, . . . , f n. You might find the max function defined
in Section 3.4 useful.
To test this function, add to your script a definition of some values of f thus:
f 0 = 0
f 1 = 44
f 2 = 17
f _ = 0
88 CHAPTER 4. DESIGNING AND WRITING PROGRAMS
4.22 Given a function f of type Integer -> Integer give a recursive definition of
a function of type Integer -> Bool which on input n returns True if one or
more of the values f 0, f 1, . . . , f n is zero and False otherwise.
4.23 Can you give a definition of regions which instead of being recursive uses the
function sumFun?
4.24 [Harder] Find out the maximum number of pieces we can get by making a
given number of flat (that is planar) cuts through a solid block. It is not the
same answer as we calculated for straight-line cuts of a flat piece of paper.
blackSquares n
| n<=1 = black
| otherwise = black ‘beside‘ blackSquares (n-1)
or diagrammatically,
`beside`
Suppose that we want to build a line of alternating black and white squares: we get
this by putting a black square on the front of a line beginning with a white square:
blackWhite n
| n<=1 = black
| otherwise = black ‘beside‘ whiteBlack (n-1)
where whiteBlack is the function that builds a line of alternating squares, begin-
ning with a white. Diagrammatically this is given by
`beside`
4.6. EXTENDED EXERCISE: PICTURES 89
blackChess n m
| n<=1 = blackWhite m
| otherwise = blackWhite m ‘above‘ whiteChess (n-1) m
where the corresponding function whiteChess builds a board with a white square
in the top left-hand corner. Diagrammatically,
`above`
Exercises
4.25 Complete the definitions of whiteBlack and whiteChess.
so that
Hint: you might want to use some of the functions defined here, or
variants of them, in writing your definition.
Example
1. The sequence of Fibonacci numbers starts with 0 and 1, and subsequent values
are given by adding the last two values, so that we get 0+1=1, 1+1=2 and so forth.
This can be given a recursive definition as follows
where we see in the general case that fib n depends upon not only fib (n-1) but
also fib (n-2).
This gives a clear description of the Fibonacci numbers, but unfortunately it
gives a very inefficient program for calculating them. We can see that calculating
fib n requires us to calculate both fib (n-2) and fib (n-1), and in calculating
fib (n-1) we will have to calculate fib (n-2) again. We look at ways of overcom-
ing this problem in Section 5.2.
2. Dividing one positive integer by another can be done in many different ways.
One of the simplest ways is repeatedly to subtract the divisor from the number being
divided, and we give a program doing that here. In fact we will define two functions
remainder 37 10 = 7
divide 37 10 = 3
If we subtract the divisor, 10, from the number being divided, 37, how are the values
related?
remainder 27 10 = 7
divide 27 10 = 2
The remainder is the same, and the result of the division is one less. What happens
at the base case? An example is
remainder 7 10 = 7
divide 7 10 = 0
remainder m n
| m<n = m
| otherwise = remainder (m-n) n
divide m n
| m<n = 0
| otherwise = 1 + divide (m-n) n
remainder 7 0
; remainder (7-0) 0
; remainder 7 0
; ....
This calculation will loop for ever, and indeed we should expect problems if we try
to divide by zero! However, the problem also appears if we try to divide by a negative
number, for instance
divide 4 (-4)
; divide (4-(-4)) (-4)
; divide 8 (-4)
; ...
The lesson of this example is that in general there is no guarantee that a function de-
fined by recursion will always terminate. We will have termination if we use prim-
itive recursion, and other cases where we are sure that we always go from a more
complex case to a simpler one; the problem in the example here is that subtracting
a negative number increases the result, giving a more complex application of the
function.
Exercises
4.31 Give a recursive definition of a function to find the highest common factor of
two positive integers.
4.32 Suppose we have to raise 2 to the power n. If n is even, 2*m say, then
2n = 22*m = (2m )2
2n = 22*m+1 = (2m )2 *2
• two items equal, the third different. In fact, this represents two cases
94 CHAPTER 4. DESIGNING AND WRITING PROGRAMS
6 4 1
6 6 6
2 6 6
2 2 6
If we test our definition in Section 3.4 with these data then we see that the program
gives the right results.
We can code these tests in the HUnit unit testing framework, like this:
• A String printed in case the test fails, e.g. "for: maxThree 6 4 1".
mysteryMax 6 6 2 ; 2
If we add the following test case to the test set:
testMax5 = TestCase (assertEqual "for: mysteryMax 6 6 2" 6 (mysteryMax 6 6 2))
testsMMax = TestList [testMMax1, testMMax2, testMMax3, testMMax4, testMMax5]
and run the tests, we get these results:
*Chapter4> runTestTT testsMMax
### Failure in: 4
for: mysteryMax 6 6 2
expected: 6
but got: 2
Cases: 5 Tried: 5 Errors: 0 Failures: 1
Counts {cases = 5, tried = 5, errors = 0, failures = 1}
This is an important example: it tells us that testing alone cannot assure us that a
function is correct. How might we have spotted this error in designing our test data?
We could have said that not only did we need to consider the groups above, but that
we should have looked at all the different possible orderings of the data, giving
• all three values different: six different orderings;
• two items equal, the third different. In each of the two cases we consider three
orderings.
The final case generates the test data 6 6 2 which find the error.
We mentioned special cases earlier: we could see this case of two equal to the
maximum in this way. Clearly the author of mysteryMax was thinking about the
general case of three different values, so we can see the example as underlining the
importance of looking at special cases.
• If a function uses recursion we should test the zero case, the one case and the
general case.
In the example of mysteryMax we should be guided to the data 6 6 2 since the first
two inputs are at the boundaries of the guards
x > y && x > z y > x && y > z
We take up the ideas discussed in this section when we discuss proof in Chapter 9.
96 CHAPTER 4. DESIGNING AND WRITING PROGRAMS
prop_fact n =
fact n > 0
But if we try this out, we get this result
*Chapter4> fact 17
-288522240
This is because Int is a fixed-size representation of integers, and when numbers
become big enough, they ‘wrap around’ into the negative.
The lesson of this example is that if you expect the integers in your program to be-
have like the ‘real’ integers, then you should use Integer rather than Int.
Exercises
4.34 Use the test data from the previous question to test the function
solution m n p = ((m+n+p)==3*p)
should return True only if all its inputs are different. Devise black box test data
for this function.
4.8. PROGRAM TESTING 97
using the test data written in the previous question. What do you conclude on
the basis of your results?
which returns how many of its three integer inputs are larger than their aver-
age value.
4.38 Devise test data for a function to raise two to a positive integer power.
4.39 Repeat these exercises to define QuickCheck properties which can be used to
test these functions.
Summary
This chapter has introduced some general principles of program design.
• We should think about how best to use what we already know. If we have al-
ready defined a function f we can make use of it in two ways.
• We should think about how to break the problem into smaller, more easily
solved, parts. We should ask What if I had ...?.
• We can define data types to model our problem domain. We’ll come back
to this in later chapters, where we’ll discover ways of building complex types
from simpler ones, as well as how to define more complex data types.
We also explained the basics of recursion, and saw how it is used in practice to define
a variety of functions. We shall see many more illustrations of this when we look at
recursion over lists in Chapter 7.
We concluded by showing that it was possible to think in a principled way about
designing test data for function definitions rather than simply choosing the first data
that came to mind.
98 CHAPTER 4. DESIGNING AND WRITING PROGRAMS
Chapter 5
Thus far we have looked at programs which work over the basic types Integer, Int,
Float, Bool and Char, and we have also seen how to design enumerated types like
the Move type in the Rock - Paper - Scissors game. We’ve also looked at strategies
for designing programs in general, including breaking problems down into smaller
problems to solve them in steps. However, in practical problems we will want to
represent more complex things, as we saw with our Picture example in Chapter 1.
This chapter introduces two ways of building compound data built into Haskell;
these are the tuple and the list, and in particular the String type. We’ll also look
again at ways of defining data types for ourselves. Together they are enough to let
us represent many different kinds of ‘structured’ information. We shall meet other
ways of defining data types for ourselves in Chapters 14 and 16.
After looking at these various types, we’ll explain how to manipulate tuples and
lists, and in particular we introduce the ‘list comprehension’ notation to write down
descriptions of how lists may be formed from other lists, and use this in a database
case study.
In the chapters to come we look at the range of built-in list processing functions,
aw well as how list-manipulating functions can be defined from scratch.
• In a list we combine an arbitrary number of values – all of the same type – into
a single object.
Let’s look at an example to clarify the difference. Suppose that we want to make a
simple model of a supermarket, and as part of that model we want to record the
contents of someone’s shopping basket.
99
100 CHAPTER 5. DATA TYPES, TUPLES AND LISTS
Individual items
A given item has a name and a price (in pence), and we therefore need somehow to
combine these two pieces of information. We do this in a tuple, such as
("Salt: 1kg",139)
("Plain crisps",25)
We can give names to types in Haskell, so that types are made easier to read, and we
name this tuple type ShopItem like this:
How are the contents of the basket represented? We know that we have a collec-
tion of items, but we do not know in advance how many we have; one basket might
contain ten items, another one three; a third might be empty. Each item is repre-
sented in the same way, as a member of the ShopItem type, and so we represent the
contents of the basket by a list of these, as in the list
[ (String,Int) ]
[ ShopItem ]
Other members of this list type include the empty list, [], and the basket above with
a second packet of crisps replacing the gin:
Naming types
As we have seen, we can give names to types in Haskell, as in the definition
We now look at tuple types in more detail, and examine some examples of how tu-
ples are used in practice.
Example
1. First, we can use a tuple to return a compound result from a function, as in the
example where we are required to return both the minimum and the maximum of
two Integers
minAndMax :: Integer -> Integer -> (Integer,Integer)
minAndMax x y
| x>=y = (y,x)
| otherwise = (x,y)
One way of dealing with this is for the function to return a (Float,Bool) pair.
If the boolean part is False, this signals that no solution was found; if it is like
(2.1,True), it indicates that 2.1 is indeed the solution.
Pattern matching
Next we turn to look at how functions can be defined over tuples. Functions over
tuples are usually defined by pattern matching. Instead of writing a variable for an
argument of type (Integer,Integer), say, a pattern, (x,y) is used.
5.2. TUPLE TYPES 103
name (n,p) = n
price (n,p) = p
Haskell has these selector functions on pairs built in. They are
fst (x,y) = x
snd (x,y) = y
Given these selector functions we can avoid pattern matching if we so wish. For
instance, we could redefine addPair like this
addPair :: (Integer,Integer) -> Integer
addPair p = fst p + snd p
but generally a pattern-matching definition is easier to read than one which uses
selector functions instead.
Example
3. We first introduced the Fibonacci numbers
0, 1, 1, 2, 3, 5, ... , u, v, (u+v), ...
in Section 4.7, where we gave an inefficient recursive definition of the sequence.
Using a tuple we can give an efficient solution to the problem. The next value in the
sequence is given by adding the previous two, so what we do is to write a function
which returns two consecutive values as a result. In other words we want to define a
function fibPair so that it has the property that
104 CHAPTER 5. DATA TYPES, TUPLES AND LISTS
fibStep has a single argument which is a pair of numbers, while fibTwoStep has
two arguments, each of which is a number. We shall see later that the second func-
tion can be used in a more flexible way than the first; for the moment it is important
to realize that there is a difference, and that type errors will result if we confuse the
two and write
Exercises
which returns the maximum of two integers, together with the number of
times it occurs. Using this, or otherwise, define the function
5.3. INTRODUCING ALGEBRAIC TYPES 105
which puts the elements of a triple of three integers into ascending order. You
might like to use the maxThree, middle and minThree functions defined ear-
lier.
5.3 Define the function which finds where a straight line crosses the x-axis. You
will need to think about how to supply the information about the straight line
to the function.
5.4 Design test data for the preceding exercises; explain the choices you have
made in each case. Give a sample evaluation of each of your functions.
Enumerated types
We have already seen examples of this, such as the type modelling a move in the
Rock - Paper - Scissors game, in Section 4.3. Recall that the definition lists the ele-
ments of the type, thus:
Product types
Instead of using a tuple we can define a data type with a number of components or
fields, often called a product type. An example might be
data People = Person Name Age (People)
deriving (Eq,Show)
where Name is a synonym for String, and Age for Int, written thus:
type Name = String
type Age = Int
The definition of People should be read as saying
To construct an element of type People, you need to supply two values; one, st
say, of type Name, and another, n say, of type Age. The element of People formed
from them will be Person st n.
Example values of this type include
Person "Electric Aunt Jemima" 77
Person "Ronnie" 14
As before, functions are defined using pattern matching. Any element of type People
will have the form Person st n, and so we can use this pattern on the left-hand side
of a definition,
showPerson :: People -> String
showPerson (Person st n) = st ++ " -- " ++ show n
(recall that show gives a textual form of an Int). For instance,
showPerson (Person "Electric Aunt Jemima" 77)
= "Electric Aunt Jemima -- 77"
Elements of the People type are made (or constructed) by applying the construc-
tor Person. This is called a binary constructor because it takes two values to form a
value of type People. For the enumerated types like Move the constructors are called
nullary (or 0-ary) as they take no arguments.
The constructors introduced by algebraic type definitions can be used just like
functions, so that Person st n is the result of applying the function Person to the
arguments st and n; we can interpret the definition (People) as giving the type of
the constructor, here
5.3. INTRODUCING ALGEBRAIC TYPES 107
• Each object of the type carries an explicit label of the purpose of the element;
in this case that it represents a person.
• It is not possible accidentally to treat an arbitrary pair consisting of a string
and a number as a person; a person must be constructed using the Person
constructor.
• The type will appear in any error messages due to mis-typing; a type synonym
might be expanded out and so disappear from any type error messages.
There are also advantages of using a tuple type, with a synonym declaration.
In each system that we model we will have to choose between these alternatives: our
decisions will depend exactly on how we use the products, and on the complexity of
the system.
The approach here works equally well with unary constructors, so we might say
whose elements are Years 45 and so on. It is clear from a definition like this that 45
is here being used as an age in years, rather than some unrelated numerical quantity.
The disadvantage is that we cannot use functions defined over Int directly over Age.
The examples of types given here are a special case of what we look at next.
Alternatives
A shape in a simple geometrical program is either a circle or a rectangle. These al-
ternatives are given by the type
which says that there are two ways of building an element of Shape. One way is to
supply the radius of a Circle; the other alternative is to give the sides of a Rectangle.
Example objects of this type are
Circle 3.0
Rectangle 45.9 87.6
Pattern matching allows us to define functions by cases, as in
isRound :: Shape -> Bool
isRound (Circle _) = True
isRound (Rectangle _ _) = False
and also lets us use the components of the elements:
area :: Shape -> Float
area (Circle r) = pi*r*r
area (Rectangle h w) = h*w
Another way of reading the definition (Shape) is to say that there are two construc-
tor functions for the type Shape, whose types are
Circle :: Float -> Shape
Rectangle :: Float -> Float -> Shape
These functions are called constructor functions because the elements of the type
are constructed by applying these functions.
Extensions of this type, to accommodate the position of an object, are discussed
in the exercises at the end of this section.
• The types can be recursive; we can use the type we are defining, Typename, as
(part of) any of the types tij . This gives us lists, trees and many other data
structures.
• The Typename can be followed by one or more type variables which may be
used on the right-hand side, making the definition polymorphic.
Recursive polymorphic types combine these two ideas, and this powerful mixture
provides types which can be reused in many different situations – the built-in type
of lists is an example of this kind of type. We look at these in Chapter 14.
For the moment, however, we’ll deal with the simple cases we have covered in
this section, which are enough to model a wide variety of different problem do-
mains, particularly in conjunction with tuples and lists.
Exercises
5.5 Define a function to give the length of the perimeter of a geometrical shape, of
type Shape. What is the type of this function?
5.6 Re-define the Item type for supermarket products so that it uses a data defi-
nition rather than a type definition.
5.7 Add an extra constructor to Shape for triangles, and extend the functions isRound,
area and perimeter to include triangles.
110 CHAPTER 5. DATA TYPES, TUPLES AND LISTS
5.8 Define a function which decides whether a Shape is regular: a circle is regular,
a square is a regular rectangle and being equilateral makes a triangle regular.
5.9 Investigate the derived definitions for Move and Shape: what form do the show
functions take, for example?
5.10 Define an == function over Shape so that all circles of negative radius are
equated. How would you treat rectangles with negative sides?
5.11 The type Shape takes no account of the position or orientation of a shape.
After deciding how to represent points, how would you modify the original
definition of Shape to contain the centre of each object? You can assume that
rectangles lie with their sides parallel to the axes, thus:
(Rectangle h w ...)
5.14 Some houses have a number; others have a name. How would you implement
the type of ‘strings or numbers’ used as a part of an address? Write a function
which gives the textual form of one of these objects. Give a definition of a type
of names and addresses using the type you have defined.
5.4. OUR APPROACH TO LISTS 111
[1,2,3,4,1,4] :: [Integer]
[True] :: [Bool]
[’a’,’a’,’b’] :: String
"aab" :: String
2 This was essentially the approach taken in the first edition of this book.
112 CHAPTER 5. DATA TYPES, TUPLES AND LISTS
We can build lists of items of any particular type, and so we can have lists of func-
tions and lists of lists of numbers, as in
As a special case the empty list, [], which contains no items, is an element of every
list type.
The order of the items in a list is significant, as is the number of times that an
item appears. The three lists of numbers which follow are therefore all different:
[1,2,1,2,2]
[2,1,1,2,2]
[2,1,1,2]
The first two have length 5, while the third has length 4; the first element of the first
list is 1, while the first element of the second is 2. A set is another kind of collection
in which the ordering of items and the number of occurrences of a particular item
are not relevant; we look at sets in Chapter 16.
There are some other ways of writing down lists of numbers, characters and
other enumerated types
[2 .. 7] ; [2,3,4,5,6,7]
[3.1 .. 7.0] ; [3.1,4.1,5.1,6.1,7.1]
[’a’ .. ’m’] ; "abcdefghijklm"
• [n,p .. m] is the list of numbers whose first two elements are n and p and
whose last is m, with the numbers ascending in steps of p-n. For example,
[7,6 .. 3] ; [7,6,5,4,3]
[0.0,0.3 .. 1.0] ; [0.0,0.3,0.6,0.8999999999999999]
[’a’,’c’ .. ’n’] ; "acegikm"
• In both cases it can be seen that if the step size does not allow us to reach m
exactly, the last item of the list is the element in the sequence that is closest
to m, even if it appears to “overshoot” the limit. It can also be the case that
rounding errors on Float lead to lists being different from what is anticipated;
an example is given in the exercises.
In the next section we turn to a powerful method of writing down lists which we can
use to define a variety of list-manipulating functions.
5.6. LIST COMPREHENSIONS 113
Exercises
5.15 What value has the expression [0, 0.1 .. 1]? Check your answer in GHCi
and explain any discrepancy there might be between the two.
5.16 How many items does the list [2,3] contain? How many does [[2,3]] con-
tain? What is the type of [[2,3]]?
5.17 What is the result of evaluating [2 .. 2]? What about [2,7 .. 4]? Try eval-
uating [2,2 .. 2]; to interrupt evaluation in GHCi under Windows or Unix
you need to type Ctrl-C.
Example
1. Suppose that the list ex is [2,4,7], then the list comprehension
will be
[4,8,14]
as it contains each of the elements n of the list ex, doubled: 2*n. We can read (1) as
saying
where the symbol <- is meant to resemble the mathematical symbol for being an
element, ‘2’. We can write the evaluation of the list comprehension in a table, thus:
n = 2 4 7
2*n = 4 8 14
2. In a similar way,
3. We can combine a generator with one or more tests, which are Boolean expres-
sions, thus:
(2) is paraphrased as
5.6. LIST COMPREHENSIONS 115
‘Take all 2*n where n comes from ex, n is even and greater than 3.’
n = 2 4 7
isEven n = T T F
n>3 = F T
2*n = 8
The result of (2) will therefore be the list [8], as 4 is the only even element of
[2,4,7] which is greater than 3.
4. Instead of placing a variable to the left of the arrow ‘<-’, we can put a pattern.
For instance,
m = 2 2 7
n = 3 1 8
m+n = 5 3 15
giving the result
m = 2 2 7
n = 3 1 8
m<n = T F T
m+n = 5 15
giving
6. Note that we can simply test elements, with the effect that we filter some of the
elements of a list, according to a Boolean condition. To find all the digits in a string
we can say
8. The pattern on the left-hand side of an arrow need not match everything in the
list: take the example
Exercises
which converts all small letters in a String into capitals, leaving the other
characters unchanged. How would you modify this function to give
which behaves in the same way except that all non-letters are removed from
the list?
which returns the list of divisors of a positive integer (and the empty list for
other inputs). For instance,
divisors 12 ; [1,2,3,4,6,12]
A prime number n is a number whose only divisors are 1 and n. Using divisors
or otherwise define a function
which checks whether or not a positive integer is prime (and returns False if
its input is not a positive integer).
which is True if the Integer is an element of the list, and False otherwise.
For the examples above, we have
Since elem is a prelude function, you need to hide it as described on page 53.
which takes a list of strings and returns a single string which when printed
shows the strings on separate lines.
which takes a string and an integer, n. The result is n copies of the string joined
together. If n is less than or equal to 0, the result should be the empty string,
"", and if n is 1, the result will be the string itself.
which takes a string and forms a string of length linelength by putting spaces
at the front of the string. If linelength were 12 then pushRight "crocodile"
would be " crocodile". How would you make linelength a parameter
of this function?
5.25 Can you criticize the way the previous function is specified? Look for a case in
which it is not defined what it should do – it is an exceptional case.
which produces a table of Fibonacci numbers. For instance, the effect of putStr
(fibTable 6) should be
n fib n
0 0
1 1
2 1
3 2
4 3
5 5
6 8
5.7. A LIBRARY DATABASE 119
Types
In modelling this situation, we first look at the types of the objects involved. People
and books are represented by strings
and then record each loan in the form Loan "Alice" "Asterix".
• We could associate with each person the list of books that they have borrowed,
using a pair (Person,[Book]).
Here we choose to make the database a list of (Person,Book) pairs. If the pair
("Alice" , "Asterix") is in the list, it means that "Alice" has borrowed the
book called "Asterix". We therefore define
exampleBase :: Database
exampleBase
= [ ("Alice" , "Tintin") , ("Anna" , "Little Women") ,
("Alice" , "Asterix") , ("Rory" , "Tintin") ]
After defining the types of the objects involved, we consider the functions which
work over the database.
120 CHAPTER 5. DATA TYPES, TUPLES AND LISTS
• Given a person, we want to find the book(s) that he or she has borrowed, if
any.
• Given a book, we want to find the borrower(s) of the book, if any. (It is assumed
that there may be more than one copy of any book.)
• Given a book, we want to find out whether it is borrowed.
• Given a person, we may want to find out the number of books that he or she
has borrowed.
Each of these lookup functions will take a Database, and a Person or Book, and
return the result of the query. Their types will be
books :: Database -> Person -> [Book]
borrowers :: Database -> Book -> [Person]
borrowed :: Database -> Book -> Bool
numBorrowed :: Database -> Person -> Int
Note that borrowers and books return lists; these can contain zero, one or more
items, and so in particular an empty list can signal that a book has no borrowers, or
that a person has no books on loan.
Two other functions need to be defined. We need to be able to model a book be-
ing loaned to a person and a loaned book being returned. The functions modelling
these will take a database, plus the loan information, and return a different database,
which is the original with the loan added or removed. These update functions will
have type
makeLoan :: Database -> Person -> Book -> Database
returnLoan :: Database -> Person -> Book -> Database
Note that in this definition Person is a type while person is a variable of type Person.
As we said at the start, books forms a model for the other lookup functions,
which we leave as an exercise.
We have used the ++ operator here to join two lists, namely the one element list
[(pers,bk)] and the ‘old’ database dBase.
To return a loan, we need to check through the database, and to remove the pair
(pers,bk). We therefore run through all the pairs in the database, and retain those
which are not equal to (pers,bk), thus
Note that we have used a simple variable pair rather than a pattern to run over the
pairs in the dBase. This is because we do not need to deal with the components
separately; all we do is check whether the whole pair is equal to the pair (pers,bk).
On the other hand we could use a pattern thus:
Testing
A Haskell interpreter acts like a calculator, and this is useful when we wish to test
functions like those in the library database. Any function can be tested by typing
expressions to the GHCi prompt. For example,
To test more substantial examples, it is sensible to put test data into a script, so we
might include the definition of exampleBase as well as various tests
test1 :: Bool
test1 = borrowed exampleBase "Asterix"
test2 :: Database
test2 = makeLoan exampleBase "Alice" "Rotten Romans"
and so on. Adding them to the script means that we can repeatedly evaluate them
without having to type them out in full each time. Another device which can help is
to use it, which is short for ‘the last expression evaluated’ in GHCi. The following
sequence makes a loan, then another, then returns the first.
Testing in QuickCheck
We can use QuickCheck to test the database, too. The Chapter5 module has a num-
ber of properties, but here we include two basic ones:
• If we loan bk to pers and then lookup the books loaned to pers, then bk
should be in that list:
5.7. A LIBRARY DATABASE 123
• If we return the loan of bk to pers and then lookup the books loaned to pers,
then bk should be not in that list:
Exercises
5.28 Define the functions borrowers, borrowed and numBorrowed. To define numBorrowed
you will probably need the length function which returns the length of a list.
5.30 How would you have to modify the database functions if you had used the type
Would you expect this property to hold? If so, why? If not, why not, and how
would you modify it so that it does hold?
124 CHAPTER 5. DATA TYPES, TUPLES AND LISTS
5.32 Discuss how you would implement the database functions had you used the
representation [(Person,[Book])] rather than [(Person,Book)]for the database.
5.33 How would the tests for the database have to be modified to work with the im-
plementation defined in the previous question? Would the QuickCheck prop-
erties have to be modified: if so, how? If not, why not?
5.34 Define functions to give more readable output from the database operations
of this section.
Summary
This chapter has introduced the structured types of tuples and lists, and explained
their differences: in a given tuple type, (t1 ,...tn ) the elements all have the same
form, namely (v1 ,...vn ), with each component vi being a member of the corre-
sponding type ti . The list type [t] on the other hand contains elements [e1 ,...,en ]
of different lengths but in which all the values ei have the same type t.
Over tuples we introduced the notion of pattern matching – in which a pattern
such as (x,y) could be used to stand for an arbitrary member of a pair type – and
saw how this led to more readable definitions.
We also saw how we could define our own data types to model product types –
like tuples – and sums, which can represent types containing a number of different
alternative elements.
The bulk of the chapter was an account of the facilities which Haskell provides
for working with lists. These include various ways of writing lists of elements of base
type, including ranges like [2,4..12], and list comprehensions, in which the mem-
bers of a list are generated, tested and transformed from the elements of another
list, as exemplified by
which selects the alphabetic characters from a string, and converts them to up-
per case, using functions imported from the module Data.Char. We also saw that
String is the list type [Char].
In the chapters to come we will use the list functions given here in making our
own definitions, as well as finding out about the prelude and library functions for
lists, and how they are themselves defined.
Chapter 6
The aim of this chapter is to introduce the operations on lists contained in the pre-
lude and the libraries that come with Haskell 2010. In order to understand the types
of these library functions we have to examine how generic or polymorphic func-
tions. Polymorphism is the mechanism by which a Haskell function can act over
more than one type: the length function on lists can be used over any list type, for
instance.
After doing this we’re in a position to review the functions in the prelude, in the
Haskell 2010 libraries, those which are available in the Haskell Platform and those
others which appear on Hackage. All these are reviewed, and in particular we look
at how we can discover functions with the type or behaviour that we’re looking for.
To make use of these library functions, we then introduce a series of extended
exercises to stretch the reader rather more than the small exercises we have given
thus far. These include extensions of the Picture functions and a billing program
for a supermarket checkout, which has to produce a formatted bill from the list of
bar codes scanned in at a checkout. Finally we include the example of card games,
which shows the importance of type design in writing non-trivial programs.
and so forth. How do we write down a type for length which encapsulates this? We
say
125
126 CHAPTER 6. PROGRAMMING WITH LISTS
square x = x*x
the variable x stands for an arbitrary value, so a type variable stands for an arbitrary
type, and so we can see all the types like
square x = x*x
the x’s all stand for the same (arbitrary) value. Instances of [a]->[a]->[a] will
include
[Integer]->[Integer]->[Integer]
but not the type
[Integer]->[Bool]->[Char]
This makes sense: we cannot expect to join a list of numbers and a list of Booleans
to give a string!
On the other hand, the functions zip and unzip convert between pairs of lists
and lists of pairs, and their types involve two type variables:
[Integer]->[Bool]->[(Integer,Bool)]
where a and b are replaced by different types (Integer and Bool, here). It is, of
course, possible to replace both variables by the same type, giving
[Integer]->[Integer]->[(Integer,Integer)]
and the general type [a] -> [a] -> [(a,a)].
6.1. GENERIC FUNCTIONS: POLYMORPHISM 127
id x = x
which returns its argument unchanged. In the definition there is nothing to con-
strain the type of x – all we know about x is that it is returned directly from the
function. We know, therefore, that the output type is the same as the input, and so
the most general type will be
id :: a -> a
At work here is the principle that a function’s type is as general as possible, consis-
tent with the constraints put upon the types by its definition. In the case of the id
function, the only constraint is that the input and output types are the same.
In a similar way, in defining
fst (x,y) = x
neither x nor y is constrained at all, and so they can come from different types a and
b, giving the type
fst :: (a,b) -> a
A final example is given by
fst (x,y) = x
128 CHAPTER 6. PROGRAMMING WITH LISTS
(n,m) == (p,q)
= (n==p) && (m==q)
Exercises
6.1 Give the most general types for the functions snd and sing defined by
snd (x,y) = y
sing x = [x]
is a type for id but why it is not the most general type for this function.
What is the most general type for shift, if the type declaration is omitted?
As well as the polymorphic functions in Figure 6.1, the standard prelude provides
various operations over specific types; some of these can be seen in Figure 6.2. The
types of the functions sum and product, which are overloaded, will be discussed
further in Chapter 13.
Haskell list functions in the Prelude 129
: a -> [a] -> [a] Add a single element to the front of a list.
3:[2,3] ; [3,2,3]
!! [a] -> Int -> a xs!!nreturns the nth element of xs, starting
at the beginning and counting from 0.
[14,7,3]!!1; 7
concat [[a]] -> [a] Concatenate a list of lists into a single list.
concat [[2,3],[],[4]]; [2,3,4]
tail,init [a] -> [a] All but the first/last element of the list.
tail "word" ; "ord"
init "word" ; "wor"
replicate Int -> a -> [a] Make a list of n copies of the item.
replicate 3 ’c’ ; "ccc"
take Int -> [a] -> [a] Take n elements from the front of a list.
take 3 "Peccary" ; "Pec"
drop Int -> [a] -> [a] Drop n elements from the front of a list.
drop 3 "Peccary" ; "cary"
splitAt Int -> [a] -> ([a],[a]) Split a list at a given position.
splitAt 3 "Peccary" ; ("Pec","cary")
unzip [(a,b)] -> ([a],[b]) Take a list of pairs into a pair of lists.
unzip [(1,5),(3,6)]; ([1,3],[5,6])
Looking at Figure 6.1 we can quickly locate one function, replicate, which does
have one of these types and is indeed the function which we seek. If we want a
function to reverse a list it will have type [a] -> [a] and although there is more
than one function with this type, the search is very much narrowed by looking at
types. We’ll see a little later on (page 137) that there’s a web service called hoogle to
look up functions by type.
This insight is not confined to functional languages, but is of particular use when
a language supports polymorphic or generic functions and operators as we have
seen here.
Further functions
We have not described all the functions in the prelude for two different reasons.
First, some of the general functions are higher-order and we postpone discussion
of these until Chapter 10; secondly, some of the functions, such as zip3, are obvious
variants of things we have discussed here. Similarly, we have not chosen to enumer-
ate the functions in the library Data.List; readers should consult the library file
itself, which contains type information and comments about the effects of the func-
tions, as well as the Haddock documentation for the library: we talk in detail about
documentation in the next section.
6.3. FINDING YOUR WAY AROUND THE HASKELL LIBRARIES 131
HackageDB / Cabal
Prelude The Prelude is a part of the language standard and gives definitions of
standard types, functions and classes (which we come to later). The Prelude
is listed in full in the Haskell 2010 Language Report.
The Prelude is special because it is imported into all modules by default. We
have seen this already: if we want to re-define a Prelude function, then we
have to import the Prelude explicitly, but hiding that function.
Haskell 2010 The standard libraries in Haskell 2010 use hierarchical names: see the
note on module names on page 132 for more details of this.
The libraries are grouped by name, and are described in depth in the Haskell
2010 Language Report:
However, that’s not quite the end of the story, because some modules are defined
in such a way as to (re-)export all their “children”: an example of this is the module
Foreign.C (quoting again from the report)
The module Foreign.C combines the interfaces of all modules provid-
ing C-specific marshalling support, namely
module Foreign.C.Types
module Foreign.C.String
module Foreign.C.Error
module Data.Bits
module Foreign.Ptr
...
Data These libraries contain additional data types, e.g. Data.Array, or addi-
tional operations on existing types, such as the bitwise operations on inte-
gers in Data.Bit.
Foreign The Foreign libraries provide support for interworking with other
programming languages. This includes support for the communication of
data between languages in Foreign.C and Foreign.Marshal, as well as
facilities for pointers to foreign entities.
Numeric The Numeric library contains functions to read and print numbers
in a variety of formats.
System The System libraries have support for various forms of IO handling
(in System.IO and System.IO.Error), and functions to interact with the
6.3. FINDING YOUR WAY AROUND THE HASKELL LIBRARIES 133
http://hackage.haskell.org/
A typical package description page in Hackage is shown in Figure 6.4.
The Haskell Platform The Haskell Platform is a bundled distribution of GHCi, some
standard tools, including Cabal for building, distributing and downloading li-
braries and the Hsc2hs preprocessor to assist in Haskell/C interworking.
The Haskell Platform also comes with a large collection of packages: we’ll talk
about that terminology later in this section when we discuss Cabal and Hack-
age. These packages include the following areas.
• Data and control structures, such as sets, finite maps, bytestrings and hash
tables.
• Concurrency. including lightweight threads, MVars for thread synchronisa-
tion and channels.
• Testing and debugging, including HUnit and QuickCheck as we use here.
• Network, system and web programming. These include facilities for HTTP
and socket programming, as well as access to many OS and lower-level op-
erations.
• Other packages cover text processing, graphics and math programming.
Cabal
Cabal is a command-line tool for installing packages – and the packages that they
depend on – in your system. Cabal is itself installed as a part of the Haskell Platform.
As a user you’ll need to know these cabal commands.
cabal update
Update the (cached) list of available packages. It’s sensible to do this each time
you start using the cabal command.
cabal help
Give a summary of all the available cabal commands.
ghc-pkg list
List all the packages that are currently installed.
Further information about the system, including how to install it manually if you are
not using the Haskell Platform, is available at these sites:
http://haskell.org/haskellwiki/Cabal-Install
http://hackage.haskell.org/trac/hackage/wiki/CabalInstall
file:///usr/share/doc/ghc/html/
Libraries Haddock documentation for many libraries is available from the Libraries
link on the system documentation page, or directly at
file:///usr/share/doc/ghc/html/libraries/index.html
136 CHAPTER 6. PROGRAMMING WITH LISTS
This is also inked from the Haskell Platform programs group on Windows.
file:///usr/share/doc/ghc/html/libraries/base-XXXXX/Prelude.html
Here XXXXX is the particular version of the base libraries: for the Haskell Plat-
form at the time of writing it is base-4.2.0.2.
http://hackage.haskell.org/package/
Hoogle search Hoogle allows you to search many of the standard libraries: what is
cool about Hoogle is that you can search by type as well as by name. Narrow-
ing down a search by type can lead you very quickly to your answer, or at least
eliminate a lot of “noise” in a search.
http://www.haskell.org/hoogle/
See Figure 6.6 for example results. Be aware, though, that Hoogle doesn’t cover
all the packages in Hackage.
Hayoo! search Hayoo! does search the whole of Hackage, but it is a string-based
search only.
http://holumbus.fh-wedel.de/hayoo/hayoo.html
You can also use google to search the web, for example using a type signature.
This is successful more often than you might think.
GHCi Once you have asked to import a module (Foo, say) into GHCi you can then
find out the types of all its exported functions using the GHCi command
:browse Foo
138 CHAPTER 6. PROGRAMMING WITH LISTS
:info foo
Online resources You can go online to ask questions: there’s a really lively commu-
nity on the Haskell Cafe mailing list, [email protected]: you can
find out more about how to join this list in the Communities section on the
homepage of the Haskell website www.haskell.org: you can also find links
to the Haskell IRC channel and Haskell on Stack Overflow here too.
Online texts Finally, you can find a number of texts online, including Learn You
a Haskell for Great Good, http://learnyouahaskell.com/ and the Haskell
wikibook, http://en.wikibooks.org/wiki/Haskell.
[ f x | x <- xs ]
We shall see that this operation is itself a higher-order function in Chapter 10 below.
Next we explore how to place two pictures side by side. What we want to do is to
join up the corresponding lines of the two pictures, as illustrated on page 22. How
can we accomplish this? We can see this as like flipV, in that we want to do some-
thing to every pair of lines – namely join them with ++ – but we need to associate
corresponding lines before we do this. That is exactly the purpose of the prelude
function zip, which takes two lists and pairs corresponding elements, and so we
can say
The effect of zip is to chop the list of pairs to the shorter of the two inputs, and so
beside will clip the bottom lines off whichever picture is the longer; if they are the
same length, then there is no clipping. We can also use the higher-order zipWith to
define beside; we revisit this in Chapter 9.
In our pictures, white is represented by the dot ‘.’ and black by the hash symbol
‘#’. To invert the colour of a single character we define
The characters ‘.’ and ‘#’ are swapped by this definition (and any other character is
transformed into ‘.’, too). Now, how do we invert the colours in a whole picture? We
need to invert each character in a line, using
but our use of the auxiliary function invertLine makes the previous definition
more readable.
In the next section we extend our model of pictures to give them a position as
well as some pictorial content.
propAboveBeside nw ne sw se =
(nw ‘beside‘ ne) ‘above‘ (sw ‘beside‘ se)
==
(nw ‘above‘ sw) ‘beside‘ (ne ‘above‘ se)
If we test this, then it fails. Why? Remember that we’re using the built-in facilities
of QuickCheck to generate random values from [String]. If we look at a sample of
the data,2 the results look like this:
2 We do this using sample (arbitrary :: Gen [String]).
The Picture example: implementation 141
[]
["a1","\EOT"]
[]
["","p\DC3=","\229\a\183","\218\SOH\194",""]
["g}","P","_y","\169\131\FS\t","U\nl",":YicLX\194\198","\t3"]
...
The elements are random, the pictures are generally not rectangular – because within
each list the strings are of different length – it is also very likely that four of these cho-
sen randomly will not have the right dimensions to be put together using beside
and above. We’ll see in Section 19.6 how to define random generators of data for
ourselves, and we’ll see there how to generate ‘sensible’ random pictures, built up
from ’.’ and ’#’ in rectangular patterns, and indeed to generate sets of pictures
containing random data but all of the same size.
In the meantime we can say that we only want to check a property when the
generated data satisfy some conditions: let’s take a look at an example.
propAboveBeside3Correct w e =
(rectangular w && rectangular e && height w == height e)
==>
(w ‘beside‘ e) ‘above‘ (w ‘beside‘ e)
==
(w ‘above‘ w) ‘beside‘ (e ‘above‘ e)
In writing this property we have used ==> which we can read as ‘implies’. The prop-
erty following the ==> is only checked when the Boolean condition before the arrow
is true. Without the condition the property fails: try it out!
Exercises
so that the superimposition of ‘.’ with itself gives ‘.’ while any other combi-
nation of characters gives ‘#’.
which takes two lines – which you can assume are of the same length – and su-
perimposes their corresponding characters using superimposeChar, so that,
for example,
which superimposes two pictures, which you may assume have the same di-
mensions.
6.7 Using the function putStr :: String -> IO () and any other functions
you might need, define the function
.##.
.#.#
.###
####
".##.\n.#.#\n.###\n####\n"
which rotates a picture through 90± clockwise. For instance, the effect of rotate90
on the picture in the previous exercise would be to give
#...
####
##.#
###.
Hint: you need to make a line of the new picture by picking out the ith el-
ements in each of the lines of the original picture, reflected in a horizontal
mirror.
6.9 Using rotate90 or otherwise, define a function which rotates a picture through
90± anticlockwise.
6.5. EXTENDED EXERCISE: ALTERNATIVE IMPLEMENTATIONS OF PICTURES 143
which scales the input picture by the integer provided as the second argument.
For instance, if exPic is the picture
#.#
..#
##..##
##..##
....##
....##
In the case of a zero or negative scale factor, you should return an empty pic-
ture.
6.12 Define properties which describe how beside interacts with flipH and flipV.
6.13 One property we can show holds is that if we take the same picture and put
four copies of it together using beside and above in the two different ways,
then the results are the same. Express this as a quick check property.
6.14 You can test your implementation of rotate90 using QuickCheck. Can you
think of properties which only use rotate90 and others that use rotate? You
may want to impose a condition that any picture involved is rectangular: the
function is given in the program code for this chapter.
6.15 What property would you expect invertColour to have? Can you be sure that
this will hold for randomly generated data?
.......##..........##...
.....##..#.......##..#..
...##.....#....##.....#.
..#.......#...#.......#.
..#...#...#...#...#...#.
..#...###.#...#...###.#.
.#....#..##..#....#..##.
..#...#.......#...#.....
...#...#.......#...#....
....#..#........#..#....
.....#.#.........#.#....
......##..........##....
.......##...
.....##..#..
...##.....#.
..#.......#.
..#...#...#.
..#...###.#.
.#....#..##.
..#...#.....
...#...#....
....#..#....
.....#.#....
......##....
Incompatible combinations
In earlier discussions of the Picture type, we have made the assumption that binary
functions like above have been called on arguments of compatible size: in the case
of above this would mean that the width of the two arguments was the same. What
should be done if this is not the case?
We could reasonably assume that each picture was rectangular and define the
functions so that this invariant was preserved by the binary functions. In the exam-
ple of above this requires that pictures can be padded in an appropriate way. Let’s
take the specific example of
as shown in Figure 6.7. To preserve the rectangular picture, we need to pad out the
lower as shown in Figure 6.8.
Exercises
6.5. EXTENDED EXERCISE: ALTERNATIVE IMPLEMENTATIONS OF PICTURES 145
.......##..........##...
.....##..#.......##..#..
...##.....#....##.....#.
..#.......#...#.......#.
..#...#...#...#...#...#.
..#...###.#...#...###.#.
.#....#..##..#....#..##.
..#...#.......#...#.....
...#...#.......#...#....
....#..#........#..#....
.....#.#.........#.#....
......##..........##....
.......##...............
.....##..#..............
...##.....#.............
..#.......#.............
..#...#...#.............
..#...###.#.............
.#....#..##.............
..#...#.................
...#...#................
....#..#................
.....#.#................
......##................
6.17 Redefine the functions over pictures to pad pictures in the way just described.
One way to solve the problem is to use the function
6.18 How would you work with basic pictures that were not rectangular? Define a
function which will take a picture - as a list of Strings – and return a rectan-
gular list of strings, padding out each line as necessary. Once you start with
rectangular pictures the functions you defined in the first part of this exercise
should be enough to preserve them as rectangular.
Alternative representations
In this section we look at a number of different ways that these "low fi" pictures can
be represented.
146 CHAPTER 6. PROGRAMMING WITH LISTS
Exercises
[[Bool]]
where True and False represent black and white points in a picture. How
would you have to modify the functions working over Picture to accommo-
date this change? What are the advantages and disadvantages of the two rep-
resentations?
6.20 We have represented pictures as a list of rows: how would you redefine the
functions working over pictures if they are represented as a list of columns.
6.21 [Harder] How would you re-implement the function printPicture, defined
in the solution to exercise 6.7, so that it works over this column-based repre-
sentation?
".##.\n.#.#\n.###\n####\n"
How would you redefine the picture manipulating functions over this repre-
sentation? Which functions become easier to define? Which more difficult?
Run-length encoding
A more compact representation is given by run-length encoding of Pictures, which
will code a repeated character run like "###" as a pair, (3,’#’), and a picture is
represented as a member of [[(Int,Char)]]. For example, the picture
.##.
.#.#
.###
####
Exercises
6.23 Re-implement the functions over pictures to work with this new representa-
tion. In particular, how would you print pictures which were represented in
this way?
6.24 Is it the case that all your answers to the last question give the most compact
representation, so that you don’t have adjacent runs of the same character, as
in
[(3,’.’), (1,’#’)]
If this can happen with your functions, how could you change your definitions
to avoid it?
6.25 Take another look at the QuickCheck properties you wrote to test pictures:
rewrite these for your alternative implementations. How many of the proper-
ties carry over to the alternative implementations without alteration, and how
many have to be modified in some way?
6.26 [Harder] The run-length encoding above works a line at a time, but it would
be possible to give a more compact representation which combines runs in
different lines. The earlier example could then be given by
This representation loses the length of the rows, so you would have to keep
information about the row length in the type too, giving
6.27 [Harder] If you know that only the characters ’.’ and ’#’ are used in a pic-
ture, how could you make the representation of the previous question even
more compact?
6.28 [Harder] Define two more representations of pictures yourself, and re-implement
the picture functions over these types.
148 CHAPTER 6. PROGRAMMING WITH LISTS
Basics
How can we represent pictures with positions? First we need to think about how we
model positions on an integer grid. A Position is given by a pair of integers,
Exercises
which takes an Image and returns a new Image whose Picture is unchanged
but whose Position is given by the second argument to changePosition.
so that the effect of moveImage img xMove yMove is to move img by xMove
in the horizontal (x) direction and by yMove in the vertical (y) direction.
Transformations
We can extend the transformations over the type Picture to the Image type, but we
need to think about the effect of these transformations on the position. One way
to lift the transformations from pictures to images is simply to say that the pictures
stay in the same position – we call this the naive view.
150 CHAPTER 6. PROGRAMMING WITH LISTS
If we think of reflections and rotations going on in space, then the results are
more likely to be as shown in Figure 6.10, where we see that the position of the re-
sulting image has changed. Rotation is about the reference point, and reflection is
in the horizontal or vertical line through the reference point; in general these op-
erations will change the reference point. We call this the geometrical view of the
transformations.
Exercises
6.33 Implement for Image the analogues of flipH, flipV, rotate and rotate90
under the naive view of how to lift the transformations.
6.34 Implement for Image the analogues of flipH, flipV, rotate and rotate90
under the geometrical view.
Superimposition
When pictures have positions, superimposition can be more complex. Consider the
example illustrated in Figure 6.11; here we see one way of superimposing the two
images is to use Picture superimposition on two pictures which have first been
‘padded out’ with white space as shown in the figure.
6.7. EXTENDED EXERCISE: SUPERMARKET BILLING 151
Haskell Stores
Total....................13.90
Exercises
6.35 Define functions to ‘pad out’ a Picture with an amount of white space, as
shown in Figure 6.11.
You will need to think carefully about the intended effect of the functions be-
fore you start to implement them. You will need to have function parameters
for the amount of padding to the left, right, bottom and top of the image.
Note, in particular, that the Position of an Image might change as a result of
padding.
6.36 Using the padding functions, define a superimposition function for the Image
type.
6.37 How would you use Image superimposition to give analogues of above and
beside for Images?
6.38 Define QuickCheck properties to check the implementation of the functions
over the Image type. How many carry over from the Picture type, and how
many have to be re-defined?
The problem
A scanner at a supermarket checkout will produce from a basket of shopping a list
of bar codes, like
3 I am grateful to Peter Lindsay et al. of the Department of Computer Science at the University of New
South Wales, Australia, for the inspiration for this example, which was suggested by their lecture notes.
152 CHAPTER 6. PROGRAMMING WITH LISTS
[1234,4719,3814,1112,1113,1234]
which has to be converted to a bill as shown in Figure 6.12 We have to decide first
how to model the objects involved. Bar codes and prices (in pence) can be modelled
by integers; names of goods by strings. We say therefore that
type Name = String
type Price = Int
type BarCode = Int
The conversion will be based on a database which links bar codes, names and prices.
As in the library, we use a list to model the relationship.
type Database = [ (BarCode,Name,Price) ]
The example database we use is
codeIndex :: Database
codeIndex = [ (4719, "Fish Fingers" , 121),
(5643, "Nappies" , 1010),
(3814, "Orange Jelly", 56),
(1111, "Hula Hoops", 21),
(1112, "Hula Hoops (Giant)", 133),
(1234, "Dry Sherry, 1lt", 540)]
The object of the script will be to convert a list of bar codes into a list of (Name,Price)
pairs; this then has to be converted into a string for printing as above. We make the
type definitions
type TillType = [BarCode]
type BillType = [(Name,Price)]
and then we can say that the functions we wish to define are
makeBill :: TillType -> BillType
which takes a list of bar codes to a list of name/price pairs,
formatBill :: BillType -> String
which takes a list of name/price pairs into a formatted bill, and
produceBill :: TillType -> String
which will combine the effects of makeBill and formatBill, thus
produceBill = formatBill . makeBill
The length of a line in the bill is decided to be 30. This is made a constant, thus
lineLength :: Int
lineLength = 30
Making lineLength a constant in this way means that to change the length of a
line in the bill, only one definition needs to be altered; if 30 were used in each of
the formatting functions, then each would have to be modified on changing the line
length. The rest of the script is developed through the sequences of exercises which
follow.
6.7. EXTENDED EXERCISE: SUPERMARKET BILLING 153
Exercises
6.39 Given a number of pence, 1023 say, the pounds and pence parts are given by
1023 ‘div‘ 100 and 1023 ‘mod‘ 100. Using this fact, and the show func-
tion, define a function
Recall that ’\n’ is the newline character, that ++ can be used to join two
strings together, and that length will give the length of a string. You might
also find the replicate function useful.
which applies formatLine to each (Name,Price) pair, and joins the results
together.
which takes a list of (Name,Price) pairs, and gives the total of the prices. For
instance,
Exercises
Haskell Stores
Discount..................1.00
Total....................12.90
which applies lookup to every item in the input list. For instance, when ap-
plied to [1234,4719,3814,1112,1113,1234] the result will be the list of
(Name,Price) pairs given in Exercise 6.25. Note that 1113 does not appear
in codeIndex and so is converted to ("Unknown Item",0).
This completes the definition of makeBill and together with formatBill
gives the conversion program.
Exercises
6.48 You are asked to add a discount for multiple buys of sherry: for every two bot-
tles bought, there is a 1.00 discount. From the example list of bar codes
[1234,4719,3814,1112,1113,1234]
the bill should be as illustrated in Figure 6.13. You will probably find it helpful
to define functions
6.49 Design functions which update the database of bar codes. You will need a
function to add a BarCode and a (Name,Price) pair to the Database, while
at the same time removing any other reference to the bar code already present
in the database.
6.50 Re-design your system so that bar codes which do not appear in the database
give no entry in the final bill. There are (at least) two ways of doing this.
• Keep the function makeBill as it is, and modify the formatting functions,
or
• modify the makeBill function to remove the ‘unknown item’ pairs.
6.51 [Harder] How appropriate would it be to test your supermarket billing system
using QuickCheck? Could you check parts of the system using QuickCheck?
Could you use it to test the whole system, or could you do both?
Exercises
6.53 Define a type Suit to represent suits and a type Value to represent the value
of cards. Using these or otherwise, define a type Deck to represent a deck of
cards. You may use type synonyms (type) or data type definitions (data), or
both.
6.54 Try to give a rationale for the choices you have made in answering the previous
question: if you can, give alternative definitions which use the other mecha-
nism, and compare your solutions.
6.8. EXTENDED EXERCISE: CARDS AND CARD GAMES 157
Many card games involve the idea of each player playing one card N
in turn, called a trick. Traditionally for games like whist and bridge
the players are called after the points of the compass: North, South, W E
East and West. The play is in clockwise order: if East is the player to
start (to lead) then the players South, West and North follow in that
S
order.
Exercise 6.55
Define a type Player to represent the four players.
There is an important rule about how players should choose cards to play. A player
should follow suit: that is, if they can, they must play a card from the same suit as
the card that was led (the card played by the person starting). A player can only play
from a different suit if she has no cards left in the suit that was led.
Let’s have a look at an example. Suppose that each of the
players has five cards left, as shown to the right. Suppose
also that East is to lead and that she plays the Jack of Hearts.
South has no choice: he has to play the 2 of Hearts. West has
no Hearts, so she can choose any other card to play. Finally,
North has a choice of two Hearts: the King or the 7, let’s sup-
pose that he plays the King.
Who wins the trick? Let’s suppose first that there is no
trump suit. The highest Heart will win the trick, and that’s
the King. Why Hearts? Because a Heart was led by East.
If there is a trump suit, then the highest trump card wins
if any trumps have been played, but remember that you can
only play a trump if you’re unable to follow suit. If Spades were trumps in this case,
then West could win the trick by playing the 6 of Spades, no one else can play a
trump as they are able to follow suit.
Exercises
6.56 How would you represent a trick? Define a type Trick, using either a type or
a data definition. Remember that you need to know which player has played
which card, and also who led.
which decides the winner of a trick, assuming that there is no trump suit.
which decides the winner of a trick, assuming that there is a trump suit, which
is passed in as the first argument.
158 CHAPTER 6. PROGRAMMING WITH LISTS
6.59 Define a type Hand which represents the collection of cards held by a player at
one point during a game. In the earlier example, the hand held by North is ƒA,
2; ~K, 7; }3 (and no | cards).
6.60 Define a type Hands which describes the hands held by the four players at one
point in the game. This should represent the four hands shown in the diagram
above, for example.
which checks whether the play in a particular trick is both possible and legal.
It should be possible in the sense that the card played by each player (given in
the Trick) should be in their hand, as given in the Hands. It should be legal in
following the rule that players should follow suit if they can.
What does a game look like? As there are 52 cards, when dealt to four players this re-
sults in each player receiving 13 cards. There will therefore be thirteen tricks played.
In whist and bridge pairs of players – North/South and East/West – play together, so
in an game of 13 tricks one side or the other will win.
Exercises
6.62 Define a type Team to represent the two teams North/South and East/West.
Define a function
which will give the winning team from the list of tricks, assuming that there
are no trumps. Also define the function
which checks whether the play in each trick is both possible and legal. Hint:
you will need to deduce the hand in each case from the cards played subse-
quently: once you have done this, you can use checkPlay as defined earlier.
6.8. EXTENDED EXERCISE: CARDS AND CARD GAMES 159
Summary
This chapter has shown how the combination of list comprehensions and the built-
in functions from the prelude give us a powerful repertoire of tools with which to
build definitions over particular list types. This was evident in the Picture exam-
ple as well as in the case studies, and these also gave an opportunity to see the way
in which a larger program was built as a collection of related functions. returning
a member of the same list type. The example in Section 6.8 particularly empha-
sised the process of choosing types to represent the various entities in the domain
in question.
Section 6.3 contains vital information about the libraries available in Haskell,
including how to download them, how to find out more about their contents and
how to search for functions performing particular tasks.
In the next chapter we’ll find out about how to define functions over lists for
ourselves.
160 CHAPTER 6. PROGRAMMING WITH LISTS
Chapter 7
We have already seen how to define a variety of functions over lists using a combina-
tion of list comprehensions and the built-in list processing functions in the Haskell
prelude and libraries. This chapter looks ‘under the bonnet’ and explains how func-
tions over lists can be defined by means of recursion. This will allow us to define the
prelude functions we have already been using, as well as letting us look at a wider
class of applications, including sorting and a case study of text processing.
The chapter begins with a summary of the mechanism of pattern matching, and
continues with a justification and explanation of recursion echoing the discussion
in Chapter 4. We then explore a variety of examples both of functions defined by
primitive recursion and of more general recursive functions, and conclude with the
case study mentioned earlier.
mystery 0 y = y (mystery.1)
mystery x y = x (mystery.2)
where we distinguish between the two cases by using a pattern – here the literal 0 –
instead of a variable. Just as for guards, the equations are applied sequentially, and
so (mystery.2) will only be used in cases that (mystery.1) does not apply.
Another aspect of this definition is that y is not used on the right-hand side of
(mystery.2). Because of this we do not need to give a name to the second argu-
161
162 CHAPTER 7. DEFINING FUNCTIONS OVER LISTS
ment in this case, and so we can replace the variable y with the wildcard ‘_’ which
matches anything, thus
mystery 0 y = y
mystery x _ = x
We have therefore seen that pattern matching can be used for distinguishing be-
tween certain sorts of cases in function definitions. We have also seen pattern match-
ing used to name the components of tuples, as in
joinStrings :: (String,String) -> String
joinStrings (st1,st2) = st1 ++ "\t" ++ st2
where the variables st1 and st2 will be matched with the components of any argu-
ment.
We can see the case switching and extracting components in action together in
the definition of the function to give the area of a Shape, first seen in Section 5.3:
area :: Shape -> Float
area (Circle r) = pi*r*r
area (Rectangle h w) = h*w
The two equations apply to different kinds of shape, and within each equation the
appropriate information is extracted: from a circle its radius, r, and from a rectangle
its height, h, and width, w. We see exactly this combination of case switching and
component extraction in working with lists too, as we see in the next section.
Summarizing patterns
A pattern can be one of a number of things:
• A literal value such as 24, ’f’ or True; an argument matches this pattern if it
is equal to the value.
4:2:3:[]
Note that here we use the fact that ‘:’ is right associative, so that for any values of x,
y and zs,
x:y:zs = x:(y:zs)
It is also not hard to see that 4:2:3:[] is the only way that [4,2,3] can be built
using ‘:’. The operator ‘:’, of type
Pattern-matching definitions
If we want to make a definition covering all cases of lists we can write
fun xs = ....
but more often than not we will want to distinguish between empty and non-empty
cases, as in the prelude functions
f x:xs
will be interpreted as
(f x):xs
and not as
f (x:xs)
as we would like.
where head takes the first item in a non-empty list, tail takes all but the head of a
non-empty list and null checks whether or not a list is empty.
In the definition of null the pattern (_:_) will match any non-empty list, but
it gives no names for the head and tail; when we need to name one of these, as in
tail, then a different pattern, (_:xs), is used.
It has become an informal convention in the Haskell community to write vari-
ables over lists in the form xs, ys (pronounced ‘exes’, ‘whyes’) and so on, with vari-
ables x, y, . . . ranging over their elements. We will – when using short variable names
– often use that convention.
We can now go back to the final case of pattern matching. A constructor pat-
tern over lists will either be [] or will have the form (p:ps) where p and ps are
themselves patterns.
• A list will match the pattern (p:ps) if it is non-empty, and also if its head
matches the pattern p and its tail the pattern ps.
In the case of the pattern (x:xs), it is sufficient for the argument to be non-empty
to match the pattern; the head of the argument is matched with x and its tail with
xs. Let’s look at some examples in more detail.
• The list [2,3,4] will match (p:ps), because 2 is matched with p and [3,4]
with ps.
• The list [5] will not match (q:(p:ps)); this is because 5 can match with q,
but [] cannot be matched with (p:ps).
7.2. LISTS AND LIST PATTERNS 165
firstDigit st
= case (digits st) of
[] -> ’\0’
(x:_) -> x
case e of
p1 -> e1
p2 -> e2
...
pk -> ek
Exercises
7.1 Give a pattern-matching definition of a function which returns the first integer
in a list plus one, if there is one, and returns zero otherwise.
7.2 Give a pattern-matching definition of a function which adds together the first
two integers in a list, if a list contains at least two elements; returns the head
element if the list contains one, and returns zero otherwise.
166 CHAPTER 7. DEFINING FUNCTIONS OVER LISTS
7.3 Give solutions to the previous two questions without using pattern matching,
using built-in functions instead.
7.4 Give a definition of the firstDigit function without using a case expression.
import Test.QuickCheck
Of course, this is also something we can do when we have ourselves written two
different implementations of a particular function: test that they always give the
same value using a QuickCheck property.
Exercises
which gives the product of a list of integers, and returns 1 for an empty list;
why is this particular value chosen as the result for the empty list?
which give the conjunction and disjunction of a list of Booleans. For instance,
On an empty list and gives True and or gives False; explain the reason for
these choices.
fun [] = ....
fun (x:xs) = .... x .... xs .... fun xs ....
The crucial question to ask in trying to find a primitive recursive definition is:
What if we were given the value fun xs. How could we define fun (x:xs) from
it?
Example
1. By analogy with sum, many other functions can be defined by ‘folding in’ an op-
erator. The prelude functions product, and and or are examples; here we look at
how to define the prelude function concat,
concat [] = []
concat (x:xs) = ....
How do we find concat (x:xs) if we are given concat xs? Look at the example
where (x:xs) is the list [e1 ,e2 ,...,en ]. The value of concat xs is going to be
e2 ++...++en
and the result we want is e1 ++e2 ++...++en , and so we simply have to join the list
x to the front of the joined lists concat xs, giving the definition
concat [] = [] (concat.1)
concat (x:xs) = x ++ concat xs (concat.2)
Looking at the definition here we can see that (x:xs) is a list of lists, since its ele-
ment is joined to another list in (concat.2); the type of x will be the type of the re-
sult. Putting these facts together we can conclude that the type of the input is [[a]]
and the type of the output is [a]; this agrees with the type given in (concat.0).
2. How is the function ++ which we used in the previous example itself defined?
Can we use primitive recursion? One strategy we can use is to look at examples, so,
taking 2 for x and [3,4] for xs we have
[] ++ [9,8] = [9,8]
These examples suggest a definition
[] ++ ys = ys
(x:xs) ++ ys = x:(xs++ys)
Note that the type of ++ allows lists of arbitrary type to be joined, as long as the two
lists are of the same type.
• by being equal to y, or
It is this second case where we use the value elem x ys, and we make the following
primitive recursive definition of elem.
5. Suppose that we want to select the even elements from an integer list.
selectEven :: [Integer] -> [Integer]
Using a list comprehension, we can say
selectEven xs = [ x | x<-xs , isEven x ]
but can we give a primitive recursive definition of this function? For an empty list,
there are no elements to select from,
selectEven [] = [] (selectEven.1)
but what happens in the case of a non-empty list? Consider the examples
selectEven [2,3,4] = [2,4] = 2 : selectEven [3,4]
selectEven [5,3,4] = [4] = selectEven [3,4]
It is thus a matter of taking selectEven xs, and adding x to (the front of ) this only
when x is even. We therefore define
selectEven (x:xs) (selectEven.2)
| isEven x = x : selectEven xs
| otherwise = selectEven xs
6. As a final example, suppose that we want to sort a list of numbers into ascending
order. One way to sort the list
7 3 9 2
It is then a matter of inserting the head, 7, in the right place in this list, to give the
result
2 3 7 9
This gives the definition of iSort – the ‘i’ is for insertion sort.
7.4. FINDING PRIMITIVE RECURSIVE DEFINITIONS 171
iSort [] = [] (iSort.1)
iSort (x:xs) = ins x (iSort xs) (iSort.2)
To get some guidance about how ins should behave, we look at some examples.
Inserting 7 into [2,3,9] was given above, while inserting 1 into the same list gives
1 2 3 9
• in the case of 1, if the item to be inserted is no larger than the head of the list,
we cons it to the front of the list;
• In the case of 7, if the item is greater than the head, we insert it in the tail of
the list, and cons the head to the result, thus:
2 : 3 7 9
The function can now be defined, including the case that the list is empty.
iSort [3,9,2]
; ins 3 (iSort [9,2]) by (iSort.2)
; ins 3 (ins 9 (iSort [2])) by (iSort.2)
; ins 3 (ins 9 (ins 2 (iSort []))) by (iSort.2)
; ins 3 (ins 9 (ins 2 [])) by (iSort.1)
; ins 3 (ins 9 [2]) by (ins.1)
; ins 3 (2 : ins 9 []) by (ins.3)
; ins 3 [2,9] by (ins.1)
; 2 : ins 3 [9] by (ins.3)
; 2 : [3,9] by (ins.2)
; [2,3,9]
172 CHAPTER 7. DEFINING FUNCTIONS OVER LISTS
Developing this function has shown the advantage of looking at examples while try-
ing to define a function; the examples can give a guide about how the definition
might break into cases, or the pattern of the recursion. We also saw how using top-
down design can break a larger problem into smaller problems which are easier to
solve.
In the next section we look at definitions by more general forms of recursion.
Exercises
7.7 Test your implementations against the built-in definitions, using the method
outlined on page 167.
so that elemNum x xs returns the number of times that x occurs in the list xs.
Can you define elemNum without using primitive recursion, using list compre-
hensions and built-in functions instead?
so that unique xs returns the list of elements of xs which occur exactly once.
For example, unique [4,2,1,3,2,3] is [4,1]. You might like to think of two
solutions to this problem: one using list comprehensions and the other not.
7.10 Can you write a property which links elemNum and unique from the previous
two questions? Check to see whether this property does hold using QuickCheck.
7.11 Give primitive recursive definitions of the prelude functions reverse and unzip.
7.12 Can you use the iSort function to find the minimum and maximum elements
of a list of numbers? How would you find these elements without using iSort?
7.13 Design test data for the ins function. Your data should address different pos-
sible points of insertion, and also look at any exceptional cases.
which is true when its argument is sorted in ascending order. How could you
use this to write QuickCheck properties to check your iSort and ins func-
tions?
7.5. GENERAL RECURSIONS OVER LISTS 173
7.15 [Harder] Is it enough to test that the result of iSort is sorted? What other
property should a sorting function have? Can you write a QuickCheck prop-
erty to check this?
7.16 By modifying the definition of the ins function we can change the behaviour
of the sort, iSort. Redefine ins in two different ways so that
7.17 Would you need to redefine your function isSorted to deal with the two dif-
ferent variations of sorting discussed in the last question? If so, how would you
modify it; if not, explain why not.
7.18 Design test data for the duplicate-removing version of iSort, explaining your
choices.
7.19 By modifying the definition of the ins and iSort functions, define a function
to sort lists of pairs of numbers. The ordering should be lexicographic – the
dictionary ordering. This ordering first looks at the first halves of the pairs;
only if these values are equal are the second halves compared. For instance,
(2,73) is smaller than (3,0), and this is smaller than (3,2).
In defining f (x:xs) which values of f ys would help me to work out the answer?
Example
1. It is possible to use recursion over two arguments simultaneously, an example
being the definition of the prelude function zip. Recall that here we turn two lists
into a list of pairs,
If each of the lists is non-empty, we form a pair from their heads, and then zip their
tails, giving
zip (x:xs) (y:ys) = (x,y) : zip xs ys (zip.1)
but in all other cases – that is when at least one of the lists is empty – the result is
empty:
zip _ _ = [] (zip.2)
Note that we rely on the sequential nature of pattern matching here; we can give the
patterns for (zip.2) explicitly if we wish, thus:
zip (x:xs) (y:ys) = (x,y) : zip xs ys
zip (x:xs) [] = []
zip [] zs = []
and in the second definition we see the three separate cases given in three separate
equations. Using the original definition, an example calculation gives
zip [1,5] [’c’,’d’,’e’]
; (1,’c’) : zip [5] [’d’,’e’] by (zip.1)
; (1,’c’) : (5,’d’) : zip [] [’e’] by (zip.1)
; (1,’c’) : (5,’d’) : [] by (zip.2)
; (1,’c’) : [ (5,’d’) ] by defn of :
; [ (1,’c’) , (5,’d’) ] by defn of :
Note that we have used the fact that ‘:’ is right associative in writing this calculation.
2. The function take is used to take a given number of values from a list. For in-
stance,
take 5 "Hot Rats" = "Hot R"
take 15 "Hot Rats" = "Hot Rats"
In this example we do recursion over an Int and a list
take :: Int -> [a] -> [a]
There are some special cases, when the Int is zero, or the list is empty
take 0 _ = [] (take.1)
take _ [] = [] (take.2)
What about the general case, when the list is non-empty and the Int greater than
zero? We take n-1 elements from the tail of the list, and place the head on the front,
thus:
take n (x:xs)
| n>0 = x : take (n-1) xs (take.3)
and in the other cases we give an error
take _ _ = error "PreludeList.take: negative argument"
(take.4)
7.5. GENERAL RECURSIONS OVER LISTS 175
3. As a final example, we look at another method for sorting lists (of integers). The
quicksort algorithm works by generating two recursive calls to sort. Suppose we are
to sort the list
[4,2,7,1,4,5,6]
we can take off the head, 4, and then split the result [2,7,1,4,5,6] into two parts:
[2,1,4] [7,5,6]
The first contains the elements no larger than 4, the second those exceeding 4. We
sort these two, giving
[1,2,4] [5,6,7]
qSort [] = [] (qSort.1)
qSort (x:xs)
= qSort [ y | y<-xs , y<=x] ++ [x] ++ qSort [ y | y<-xs , y>x]
(qSort.2)
It is striking to see how close this program is to our informal description of the al-
gorithm, and this expressiveness is one of the important advantages of a functional
approach.
We can see that this recursion will give an answer for every finite list, since in the
recursive calls we apply qSort to two sublists of xs, which are necessarily smaller
than (x:xs).
In Chapter 20 we talk about the efficiency of various algorithms, and show that in
general quicksort will be more efficient than insertion sort. In the following section
we look at a larger example of definitions which use general forms of recursion.
Exercises
7.20 Using the definition of take as a guide, define the prelude functions drop and
splitAt. Write QuickCheck tests for these re-defined functions.
7.21 What is the value of take (-3) [] according to the definition of take given
earlier? How would you modify the definition so that there is an error reported
whenever the Int argument is negative?
7.22 The zip function takes its two arguments separately: we can define this vari-
ant to take the arguments as a pair:
176 CHAPTER 7. DEFINING FUNCTIONS OVER LISTS
There’s a built-in function, which has the reverse effect, taking a list of pairs
into a pair of lists:
Write a property for zip’ and unzip that expresses that they are the ‘inverse’
of each other: does it matter the order in which they are applied? Try both
ways, and see what happens when you apply quickCheck in each case.
7.23 How would you define a function zip3 which zips together three lists? Try to
write a recursive definition and also one which uses zip instead; what are the
advantages and disadvantages of the two different definitions?
7.24 How would you modify qSort to sort a list into descending order? How would
you ensure that qSort removed duplicate elements?
7.25 One list is a sublist of another if the elements of the first occur in the second,
in the same order. For instance, "ship" is a sublist of "Fish & Chips", but
not of "hippies".
A list is a subsequence of another if it occurs as a sequence of elements next
to each other. For example, "Chip" is a subsequence of "Fish & Chips", but
not of "Chin up".
Define functions which decide whether one string is a sublist or a subsequence
of another string.
7.26 Write QuickCheck properties which test your implementations of the tests for
‘sublist’ and ‘subsequence’.
To align the right-hand margin, the text is justified by adding extra inter-word spaces
on all lines but the last:
Overall strategy
In this section we give an example of bottom-up program development, thinking
first about some of the components we will need to solve the problem, rather than
decomposing the solution in a top-down way.
The first step in processing text will be to split an input string into words, dis-
carding any white space. The words are then rearranged into lines of the required
length. These lines can then have spaces added so as to justify the text. We therefore
start by looking at how text is split into words.
Extracting words
We first ask, given a string of characters, how should we define a function to take the
first word from the front of a string?
A word is any sequence which does not contain the whitespace characters space,
tab and newline.
whitespace = [’\n’,’\t’,’ ’]
In defining getWord we will use the standard function elem, which tests whether an
object is an element of a list. For instance, elem ’a’ whitespace is False.
To guide the definition, consider two examples.
• getWord "cat dog" is "cat". We get this by putting ’c’ on the front of "at",
which is getWord "at dog".
• the first word in the output will be given by applying getWord to st;
• the remainder will be given by splitting what remains after removing the first
word and the space following it: dropSpace (dropWord st).
The top-level function splitWords calls split after removing any whitespace at
the start of the string.
Why is the rest of the line of length len-(length w+1)? Space must be allocated
for the word w and the inter-word space needed to separate it from the word which
follows. How does the function work in an example?
getLine 20 ["Mary","Poppins","looks","like",...
; "Mary" : getLine 15 ["Poppins","looks","like",...
; "Mary" : "Poppins" : getLine 7 ["looks","like",...
; "Mary" : "Poppins" : "looks" : getLine 1 ["like",...
; "Mary" : "Poppins" : "looks" : []
; [ "Mary" , "Poppins" , "looks" ]
A companion function,
removes a line from the front of a list of words, just as dropWord is a companion
to getWord. The function to split a list of words into lines of length at most (the
constant value) lineLen can now be defined:
This concludes the definition of the function splitLines, which gives filled lines
from a list of words.
Conclusion
To fill a text string into lines, we write
Exercises
7.30 In this case study we have defined separate ‘take’ and ‘drop’ functions for words
and lines. Redesign the program so that it uses ‘split’ functions – like the pre-
lude function splitAt – instead.
7.31 [Harder] Modify the function joinLine so that it justifies the line to length
lineLen by adding the appropriate number of spaces between the words.
which when given a text string returns the number of characters, words and
lines in the string. The end of a line in the string is signalled by the newline
character, ’\n’. Define a similar function
which returns the same statistics for the text after it has been filled.
Note that punctuation and white space are ignored in the test, and that no
distinction is made between capital and small letters. You might first like to
develop a test which simply tests whether the string is exactly the same back-
wards and forwards, and only afterwards take account of punctuation and
capital letters.
so that
182 CHAPTER 7. DEFINING FUNCTIONS OVER LISTS
If the substring oldSub does not occur in st, the result should be st.
7.35 [Harder] Define QuickCheck properties which test the behaviour of your subst
function, defined in the previous question.
Summary
This chapter has shown how functions can be defined by recursion over lists, and
completes our account of the different ways that list-processing functions can be
defined. In the chapter we have looked at examples of the design principles which
we first discussed in Chapter 4, including ‘divide and conquer’ and general pieces of
advice about designing recursive programs. The text processing case study provides
a broadly bottom-up approach to defining a library of functions.
Chapter 8
This chapter completes the design and implementation of the Rock - Paper - Scissors
game. We start by looking at how strategies can be represented as functions. One of
the real strengths of Haskell is to be able to treat functions just like any other data,
and strategies are the first time we do that in practice.
Next we explore how the simplest kinds of programs, reading and writing to a
terminal, can be developed in Haskell. The model we describe forms the foundation
for more complex interactions like those in a mail system or an operating system.
We start by discussing how in the past I/O has been a problem for the users of a
functional language. The solution in Haskell is to introduce the types IO a, which
we can think of as programs that do some input/output before returning a value of
type a. These programs include simple operations to read and write information, as
well complex programs which are built from a number of IO programs sequenced
into one by means of the do construct.
We conclude the chapter by explaining how to play the Rock - Paper - Scissors
game: we show how to play one strategy against another, as well as how you can play
interactively against a strategy.
183
184 CHAPTER 8. PLAYING THE GAME: I/O IN HASKELL
([Rock,Rock,Paper],[Scissors,Paper,Rock])
Exercises
8.2 Using the function outcome that you defined in the previous exercise, define
a function
tournamentOutcome ([Rock,Rock,Paper],[Scissors,Paper,Rock]) = 1
because the scores for the three rounds are 1, -1 and 1, summing to 1.
Strategies
Suppose we want to build a program to play Rock - Paper - Scissors against an oppo-
nent. We need to find a way to describe how the machine should play, let’s call this
a strategy.
We can describe a strategy in words: something like "echo the last move" but
that’s not a program. Instead we can describe it as a function: the next move that I
will play will depend (potentially) on all the moves that my opponent has played, so
we can say
8.1. ROCK - PAPER - SCISSORS: STRATEGIES 185
Constant strategy. The simplest way of playing is always to play the same thing! So,
as strategies we have
rock _ = Rock
paper _ = Paper
scissors _ = Scissors
Each of these functions ignores its argument and returns the same move in
every situation: it should be pretty easy to beat this once an opponent realises
what’s going on.
Cycle through the possibilities. Another option is to cycle through the three possi-
bilities in turn. One way of programming that strategy is this:
cycle :: Strategy
cycle moves
= case (length moves) ‘rem‘ 3 of
0 -> Rock
1 -> Paper
2 -> Scissors
How does this work? We look at the number of moves made so far, which is
given by length moves; the remainder on dividing this by 3 cycles through
0, 1, 2 and we use a case expression to map these results to the three options
Rock, Paper, Scissors.
A random choice. We can make a random choice of play.
randomStrategy :: Strategy
This isn’t really a proper function, in fact, because it doesn’t always return the
same result, we should program this using monads, as described in Chap-
ter 18. However, a little bit of ‘under the hood’ work allows us to fake this
in Haskell; the details are in the code for this chapter. We’ll discuss the full
implementation in Section 18.2.
Echo the last move. Given the list of moves made by our opponent, it’s easy to play
by echoing their last move. However, we have to think about one thing first:
how are the moves listed in the list of moves: it could be ‘latest first’, or ‘oldest
first’. It’s actually convenient to choose latest first, so that the list of moves
186 CHAPTER 8. PLAYING THE GAME: I/O IN HASKELL
[Rock,Rock,Paper]
says that Paper was played first, followed by two Rock moves. With this settled
we can define the ‘echo’ strategy:
echo :: Strategy
We’ve had to say what is the response when there are no previous moves, and
here we’ve made the arbitrary choice of Rock.
More strategies. More strategies are described in the exercises, giving you a chance
to code them as functions of type Strategy for yourself.
Exercises
8.3 Define a strategy which plays the move which would have won against the
opponent’s last move. Define a strategy which plays the move which would
have lost against the opponent’s last move. Which of these do you think will
be a better strategy in practice? Explain your answer.
8.4 Define a strategy which plays a random choice except when the opponent’s
last two moves have been the same one. In this case we guess that her next
move will not be the same: how does that help? Well, if we know that she’s
only got two choices, we can play not to lose.
Take the concrete example of Rock being played twice by our opponent. We
guess that her next move will be Paper or Scissors: what should we play?
The answer is Scissors, because then we’ll win if she plays Paper or draw if
she also plays Scissors!
8.5 Define a strategy which looks at the frequencies that the opponent has played
the three choices. If you assume they are making random plays, then why not
predict they are about to play the least frequent, and use the trick from the
previous exercise to make a choice.
8.2. WHY IS I/O AN ISSUE? 187
8.6 [Harder] Go to the World RPS website – see the box at the end of this section –
to find out more strategies, and try to implement them.
is to combine the two strategies str1 and str2, using them alternately.
8.8 [Hard] Can you write a function which will analyse an arbitrary strategy to
work out what it does? You will need to think about how to represent this
result: is it a string describing what the strategy does, or something a bit less
informal?
8.9 [Hard] Can you use the facilities of QuickCheck – described in more detail in
Section 19.6 – to help to analyse what an arbitrary strategy does?
http://www.worldrps.com/
val :: Integer
val = 42
The effect of these definitions is to associate a fixed value with each name; in the
case of val the value is an integer and in the case of function it is a function from
integers to integers. How is an input or an output action to fit into this model?
One approach, taken in Standard ML (Milner et al. 1997) and F# (Smith 2009),
for instance, is to include operations like
inputInt :: Integer
188 CHAPTER 8. PLAYING THE GAME: I/O IN HASKELL
whose effect is to read an integer from the input; the value read in becomes the
value given to inputInt. Each time inputInt is evaluated it will be given a new
value, and so it is not a fixed integer value as it ought to be according to our original
model.
Allowing this operation into our language may not seem to cause too big a prob-
lem, but examining the example of
• Suppose that the first item input is 4, and that the next is 3. Depending upon
the order in which the arguments to ‘-’ are evaluated, the value of inputDiff
will be either 1 or -1.
Haskell provides the types IO a of I/O actions of type a or I/O programs of type
a. An object belonging to IO a is a program which will do some I/O and then return
a value of type a. Built into Haskell are some primitive I/O programs, as well as a
mechanism to sequence these I/O programs.
One way of looking at the IO a types is that they provide a small imperative pro-
gramming language for writing I/O programs on top of Haskell, without compro-
mising the functional model of Haskell itself.1
We start by looking at the basic IO capabilities built into the standard prelude,
and then we look at a whole lot of examples to see how to put these components to-
gether using the do notation. In the next section we look at how to write ‘functional
loops’ to form more complex I/O programs.
Reading input
The operation which reads a line of text from the standard input does some I/O and
returns a String which is the line just read. According to the explanation above, this
should be an object of type IO String, and indeed, the built-in function
getLine :: IO String
getChar :: IO Char
IO ()
main :: IO t
main :: IO ()
By default, this main program is expected to be in the Main module, but it can be
given in any module from the project.
Writing Strings
The operation of writing the string "Hello, World!" will be an object which per-
forms some I/O, but which has nothing of significance to pass back to the program.
It is therefore of type IO ().
The general operation to print a text string will be a function which takes the
string to be written, and gives back the I/O object which writes that string:
putStr :: String -> IO ()
and using this we can write our ‘hello, world’ program.
helloWorld :: IO ()
helloWorld = putStr "Hello, World!"
Using putStr we can define a function to write a line of output.
putStrLn :: String -> IO ()
putStrLn = putStr . (++ "\n")
The effect of this is to add a newline to the end of its input before passing it to
putStr.
Main> helloWorld
Hello, World!
Main> ...
• it is used to name2 the values returned by IO actions; this means that the later
actions can depend on values captured earlier in the program.
Together these ideas make a do expression appear like a simple imperative program,
containing a sequence of commands and assignments; although this analogy is not
complete – we examine how it breaks down in the next section – it shows that the
model of I/O given by the IO types is a familiar one, albeit in a different guise.
Example
1. We begin by looking at the definition of putStrLn from the standard prelude.
The effect of putStrLn str is to do two things: first the string str is output, then a
newline. This is accomplished by
2 or ‘capture’ or ‘bind’
192 CHAPTER 8. PLAYING THE GAME: I/O IN HASKELL
Here we see the effect of do is to sequence a number of IO actions into a single ac-
tion. The syntax of do is governed by the offside rule, and do can take any number
of arguments. We see an example of more arguments next.
2. We can write an I/O program to print something four times. The first version of
this is
put4times str
= do putStrLn str
putStrLn str
putStrLn str
putStrLn str
3. So far, we have only seen examples of output, but we can also make inputs a
part of a sequence of actions. For instance, we can read two lines of input and then
output the message "Two lines read." thus:
read2lines :: IO ()
read2lines
= do getLine
getLine
putStrLn "Two lines read."
and by analogy with Example 3 it is not difficult to see that we could write an I/O
program which reads an arbitrary number of lines.
Example
4. The last example read two lines, but did nothing with the results of the getLine
actions. How can we use these lines in the remainder of the I/O program? As part of
The do notation 193
a do program we can name the results of IO a actions. A program to read a line and
then write that line is given by
getNput :: IO ()
line := getLine
but you should be aware that there are important differences between the names
in a Haskell I/O program and the variables in an imperative program. The essential
difference is that each ‘var <-’ creates a new variable var, and so the language per-
mits ‘single assignment’ rather than the ‘updatable assignment’ familiar from the
vast majority of modern imperative languages; we look at an example of the differ-
ent in the exercises for Section 8.5.
5. We are not forced simply to output the lines we have read, unchanged, so that
we might define
reverse2lines :: IO ()
reverse2lines
= do line1 <- getLine
line2 <- getLine
putStrLn (reverse line2)
putStrLn (reverse line1)
In this example, we read two lines, and then write them in the opposite order, re-
versed.
Example
6. Example 5 can be redefined to contain local definitions of the reversed lines
reverse2lines :: IO ()
reverse2lines
194 CHAPTER 8. PLAYING THE GAME: I/O IN HASKELL
Example
7. As an example, suppose that we want to write an I/O program to read in an in-
teger value. To read an integer from a line of input we start by saying
do line <- getLine
but then we need to sequence this with an I/O action to return the line interpreted
as an Integer. We can convert the line to an integer by the expression
read line :: Integer
What we need is the IO Integer action which returns this value – this is the purpose
of return introduced in the previous section. Our program to read an Integer is
therefore
getInt :: IO Integer
Summary
This section has shown that a do expression provides a context in which to do se-
quential programming. It is possible to program complicated I/O interactions, by
sequencing simpler I/O programs. Moreover, the ‘<-’ allows us to name the value
returned by an action and then to use this named value in the remainder of the I/O
program. It is also possible to make these programs more readable by judicious use
of let definitions to name intermediate calculations.
In the next section we look at how to write repetitive I/O programs, reading all
the lines in the input, for example. We shall see that this can be done by defining a
looping construct recursively. We also discuss the way in which ‘<-’ behaves differ-
ently from the usual assignment operator.
8.5. LOOPS AND RECURSION 195
Exercises
8.10 Write an I/O program which will read a line of input and test whether the input
is a palindrome. The program should ‘prompt’ for its input and also output an
appropriate message after testing.
8.11 Write an I/O program which will read two integers, each on a separate line,
and return their sum. The program should prompt for input and explain its
output.
so that the effect of putNtimes n str is to output str, a string, n times, one
per line.
8.13 Write an I/O program which will first read a positive integer, n say, and then
read n integers and write their sum. The program should prompt appropri-
ately for its inputs and explain its output.
Example
8. The simplest copying program loops forever:
copy :: IO ()
copy =
do line <- getLine
putStrLn line
copy
The effect of copy is to read a line, and name the result line; in the next step this is
output, and the final ‘command’ is to call copy again, so looping forever. This can
be run within GHCi simply by typing copy; it can be interrupted by typing Ctrl-C.
Programs like this where the only recursive call to the function is the last state-
ment in the do block are called tail recursive. Typically tail recursive functions are
efficient to implement, because they resemble a loop in an imperative language.
196 CHAPTER 8. PLAYING THE GAME: I/O IN HASKELL
9. We can control the number of lines that are copied by passing the number as a
parameter:
copyN :: Integer -> IO ()
copyN n =
if n <= 0
then return ()
else do line <- getLine
putStrLn line
copyN (n-1)
The effect of copyN 3 should be to copy three lines of input to output. In general, if
the number of lines to be copied is less then or equal to zero – the then branch – the
program just returns (), which does no IO. On the other hand, in the else branch
we read a line, print it out, and then call copyN (n-1).
The Integer variable here is sometimes called the loop data and the value of
the loop data is usually modified in the tail recursive call. You can think of the value
of the data as like the value of a variable in an imperative language: in this case the
value decreases by one at each call, and so we ‘count down’ to the base case where
the program terminates.
10. We can also control the termination of the loop by a condition on the data; we
will copy lines until an empty line is encountered.
copyEmpty :: IO ()
copyEmpty =
do line <- getLine
if line == ""
then return ()
else do putStrLn line
copyEmpty
Here we first get a line from the input, and call it line. If it is empty we terminate
– just as we did in the last example – by calling return (). If it is not empty, we
output the line, and tail-recursively call copyEmpty.
11. Putting together what we saw in examples 9 and 10, we can count the number
of lines that we have copied, outputting this when we reach an empty line:
copyCount :: Integer -> IO ()
copyCount n =
do line <- getLine
if line == ""
then putStrLn (show n ++ " lines copied.")
else do putStrLn line
copyCount (n+1)
8.5. LOOPS AND RECURSION 197
This behaves like example 10, except that we use the loop data to keep track of the
number of lines copied. We can see this in action here:
*RPS> copyCount 0
foo
foo
bar
bar
2 lines copied.
We will now look at how these ideas can be used to program a Rock - Paper -
Scissors tournament.
Exercises
8.14 Define a wc function which copies input to output until an empty line is read.
The program should then output the number of lines, words and characters
that have been copied. [wc is a standard unix command line program.]
is recognised as a palindrome.
8.16 Write a program which repeatedly reads lines and tests whether they are palin-
dromes until an empty line is read. The program should explain clearly to the
user what input is expected and output is produced.
8.17 Write a program which repeatedly reads integers (one per line) until finding a
zero value and outputs the sum of the inputs read.
8.18 Write a program which repeatedly reads integers (one per line) until finding
a zero value and outputs a sorted version of the inputs read. Which sorting
algorithm is most appropriate in such a case?
copy :: IO ()
copy =
do
line <- getLine
let whileCopy =
do
if (line == "")
then (return ())
198 CHAPTER 8. PLAYING THE GAME: I/O IN HASKELL
else
do putStrLn line
line <- getLine
whileCopy
whileCopy
Playing interactively
You can play interactively against a particular strategy using the function
so that play rock allows you to play against the strategy which always plays Rock.
More interesting to play against is
randomPlay :: IO ()
which makes a random choice of strategy for you to play against: this is much more
of a challenge to play! A typical example in action is shown here, where you jusy
have to hit a single key – r, p, s or R, P, S – to play:
*RPS> randomPlay
r
I play: p you play: r
p
I play: r you play: p
s
I play: s you play: s
r
I play: s you play: r
p
I play: r you play: p
s
I play: p you play: s
s
I play: r you play: s
p
I play: p you play: p
r
8.6. ROCK - PAPER - SCISSORS: PLAYING THE GAME 199
where the ‘I’ is the machine, and the ‘you’ is the player. So, how do we define these
functions? The top-level definition is
play strategy =
playInteractive strategy ([],[])
playInteractive s t@(mine,yours) =
do
ch <- getChar
if not (ch ‘elem‘ "rpsRPS")
then showResults t
else do let next = s yours
putStrLn ("\nI play: " ++ show next ++
" you play: " ++ [ch])
let yourMove = convertMove ch
playInteractive s (next:mine, yourMove:yours)
The pattern here is just like the functions defined in Section 8.5: the function is
tail recursive, and the loop data is of Tournament type, and represents what has
happened in the tournament so far as a pair of lists. The function is called with
the lists (mine,yours); in the recursive call the current plays are added to give
(next:mine, yourMove:yours); remember here that the latest plays go at the
head of the list.
So, what does the function do in detail? We read a character, and call the result
of the read ch. If that’s not one of the playing characters we show the results of the
tournament (see below); if it is one, we calculate what the computer should play, by
applying its strategy s to the list of moves so far, we output what the two moves have
been, and then we call the function recursively with new loop data,
(next:mine, yourMove:yours)
This completes the implementation of the interactive Rock - Paper - Scissors game.
ch <- getChar
gives a name to the result of this IO program, which is of type Char, whereas if we
wrote
let ch = getChar
that would name the program getChar, which is of type IO Char and not Char.
so that playSvsS strat1 strat2 n plays a match of n turns between the two
strategies strat1 and strat2.
What do we need to be able to do to implement this? What will help us is a
function to add a single step to a tournament: once we have this we can apply it
repeatedly to get a game of n moves. So, we need a function like this:
step :: Strategy -> Strategy -> Tournament -> Tournament
and what this has to do is to apply the two strategies to give the next step for both
sides:
step strategyA strategyB ( movesA, movesB )
= ( strategyA movesB : movesA , strategyB movesA : movesB )
As you can see, the next move that A makes is to apply her strategy, strategyA, to
the list of moves made so far by B, movesB. This move is then added to the front of
the list of moves made by A, which we keep ‘latest first’ (remember that we decided
to keep the moves in this order when we discussed the echo strategy on page 185).
Completing the definition of playSvsS is left as an exercise.
Exercises
8.20 Complete the definition of the function playSvsS.
8.21 The result of applying playSvsS is a value of type Tournament. Define a func-
tion which prints out the results of such a tournament, including both the nu-
merical score and the sequences of moves made. Hint: it will be enough to
define a function
You can then print out the result by applying putStr to the string.
which takes the list of the opponent’s moves and computes our next move
from that. An alternative definition would be
where we have access to both our moves and our opponent’s moves in decid-
ing our next move. What are the advantages of doing this? Are there any dis-
advantages to doing it? Can you define any strategies using Strategy2 that
you couldn’t do using Strategy?
8.23 [Harder] After trying out tournaments of different strategies against each other,
can you conclude anything about what is the best strategy for playing Rock -
Paper - Scissors? Can you turn this strategy into a Strategy?
Summary
The chapter has shown how we can implement the Rock - Paper - Scissors game,
after seeing how functions can be used to represent strategies within the game. This
example has bracketed a discussion of how programs that perform I/O can be writ-
ten in Haskell without compromising its ‘purity’.
This is done by introducing the type IO a of programs which return results of
type a, distinct from the type a, just as a cheque for $100 is not the same as $100
itself: we can cash the cheque to get the money, just as we can run the program to get
its result, but the cheque (or program) and money (or result) are still very different
things.
We introduced programming with IO types through a collection of examples,
building up to writing simple interactive programs, such as echoing input to output,
and playing the Rock - Paper - Scissors game.
202 CHAPTER 8. PLAYING THE GAME: I/O IN HASKELL
Chapter 9
203
204 CHAPTER 9. REASONING ABOUT PROGRAMS
The lesson of this discussion is that we can read a function definition in (at least)
two different ways.
From this general description we are able to deduce other facts, some like (length.3)
being utterly straightforward, and others like
which was an attempted solution to the problem of finding the maximum of three
integers.
If I asked you to give me five sets of test data for the function, and for you to
test the function at those points, I would guess that you would conclude that the
implementation works: try it!
We can write a property that expresses that the function does as it should, like
this:
prop_mystery x y z =
mysteryMax x y z == (x ‘max‘ y) ‘max‘ z
Let’s see what happens when we check this property using QuickCheck:
*Chapter8> quickCheck prop_mystery
*** Failed! Falsifiable (after 91 tests and 2 shrinks):
75
75
0
*Chapter8 Test.QuickCheck> quickCheck prop_mystery
*** Failed! Falsifiable (after 4 tests and 1 shrink):
3
3
0
*Chapter8> quickCheck prop_mystery
+++ OK, passed 100 tests.
The first time we check it, it takes 91 tests to find the error; in the second case we
find it much more quickly, but in the third we don’t catch it at all!
Now let’s try to prove that the function behaves as it should. We need to look at
various cases of the ordering of the values. If we first look at the cases
x > y && x > z
y > x && y > z
z > x && z > y
then in each of these mysteryMax will produce the correct solution. In the other
cases, at least two of the three arguments are equal. If all three are equal,
x == y && y == z
the function also operates correctly. Finally, we start to look at the cases where pre-
cisely two elements are equal. The function behaves correctly when
y == z && z > x
but in the case of
x == y && y > z
we can see that the result will, erroneously, be z.
Now, we can see this process of attempting to prove a result as a general way of
testing the function – it is a form of symbolic testing which will consider all cases
in turn, at least until an error is found. We can therefore see that reasoning can give
us a powerful way of debugging programs by focusing on the reason why we cannot
complete a proof of correctness, as well as the more traditional view that a proof
shows that a program meets the requirements put upon it.
On the other hand, as we mentioned in Section 4.8, finding a proof is a difficult
enterprise, and so there are clearly roles for proof, property-based testing and tradi-
tional testing in the development of reliable software.
9.3. DEFINEDNESS, TERMINATION AND FINITENESS 207
fact (-2)
; (-2) * fact (-3)
; (-2) * ((-3) * fact (-4))
; ...
In the case that evaluation goes on for ever, we say that the value of the expression
is undefined, since no defined result is reached. In writing proofs we often have to
confine our attention to cases where a value is defined, since it is only for defined
values that many familiar properties hold. One of the simplest examples is given by
the expression
0*e
0 * fact (-2)
Finiteness
We have said nothing so far about the order in which expressions are evaluated in
Haskell. In fact, Haskell evaluation is lazy, so that arguments to functions are only
evaluated if their values are actually needed. This gives some Haskell programs a
distinctive flavour, which we explore in depth in Chapter 17. What is important for
us here is that lazy evaluation allows the definition and use of infinite lists like
[1,2,3, ... ]
and partially defined lists. In what follows we will mainly confine our attention to
finite lists, by which we mean lists which have a defined, finite length and defined
elements. Examples are
[] [1,2,3] [[4,5],[3,2,1],[]]
Reasoning about lazy programs is discussed explicitly in Section 17.9 below.
Exercises
9.1 Given the definition of fact above, what are the results of evaluating the fol-
lowing expressions?
Discuss the reasons why you think that you obtained these answers.
Assumptions in proofs
First, we look at the idea of proofs which contain assumptions. Taking a particular
example, it follows from elementary arithmetic that if we assume that petrol costs 27
pence per litre, then we can prove that four litres will cost £1.08.
What does this tell us? It does not tell us outright how much four litres will cost;
it only tells us the cost if the assumption is valid. To be sure that the cost will be
9.5. INDUCTION 209
£1.08, we need to supply some evidence that the assumption is justified: this might
be another proof — perhaps based on petrol costing £1.20 per gallon — or direct
evidence.
We can write what we have proved as a formula,
where the arrow, ), which is the logical symbol for implication, says that the second
proposition follows from the first.
As we have seen, we prove an implication like A ) B by assuming A in proving
B . If we then find a proof of A, then knowing the implication will guarantee that B is
also valid.
Yet another way of looking at this is to see a proof of A ) B as a process for turning
a proof of A into a proof of B . We use this idea in proof by induction, as one of the
tasks in building an induction proof is the induction step, where we prove that one
property holds assuming another.
square x = x*x
it is usually our intention to say that this holds for all (defined) values of the free
variable x. If we want to make this ‘for all’ explicit we can use a quantifier like this
8x (square x = x*x)
9.5 Induction
In Chapter 7 we saw that a general method for defining lists was primitive recursion,
as exemplified by
Here we give a value outright at [], and define the value of sum (x:xs) using the
value sum xs. Structural induction is a proof principle which states:
It is interesting to see that this is just like primitive recursion, except that instead of
building the values of a function, we are building up the parts of a proof. In both
cases we deal with [] as a basis, and then build the general thing by showing how
to go from xs to (x:xs). In a function definition we define fun (x:xs) using fun
xs; in the proof of P(x:xs) we are allowed to use P(xs).
Justification
Just as we argued that recursion was not circular, so we can see proof by induc-
tion building up the proof for all finite lists in stages. Suppose that we are given
proofs of P([]) and P(xs) ) P(x:xs) for all x and xs and we want to show that
P([1,2,3]). The list [1,2,3] is built up from [] using cons like this,
1:2:3:[]
and we can construct the proof of P([1,2,3]) in a way which mirrors this step-by-
step construction,
• P([]) holds;
• P([]) ) P([3]) holds, since it is a case of P(xs) ) P(x:xs);
• Recall our discussion of ‘)’ above; if we know that both P([]) ) P([3])
and P([]) hold, then we can infer that P([3]) holds.
• P([3]) ) P([2,3]) holds, and so for similar reasons we get P([2,3]).
• Finally, because P([2,3]) ) P([1,2,3]) holds, we see that P([1,2,3])
holds.
This explanation is for a particular finite list, but will work for any finite list: if the
list has n elements, then we will have n+1 steps like the four above. To conclude,
this shows that we get P(xs) for every possible finite list xs if we know that both
requirements of the induction principle hold.
A first example
We have mentioned the definition of sum; recall also the function to double all ele-
ments of a list
doubleAll [] = [] (doubleAll.1)
doubleAll (z:zs) = 2*z : doubleAll zs (doubleAll.2)
9.5. INDUCTION 211
Now, how would we expect doubleAll and sum to interact? If we sum a list after
doubling all its elements, we would expect to get the same result as by doubling the
sum of the original list:
prop_SumDoubleAll xs =
sum (doubleAll xs) == 2 * sum xs
which is identical except that we use the Boolean equality ‘==’ rather than the math-
ematical equality ‘=’; the property repeatedly passes 100 tests when we QuickCheck
it.
How are we to prove this for all xs? According to the principle of structural induction
we get two induction goals. The first is the base case
We are required to prove (base): how do we start? The only resources we have are
the equations (sum.1), (sum.2), (doubleAll.1) and (doubleAll.2), so we have
to concentrate on using these. As we are trying to prove an equation, we can think
of simplifying the two sides separately, so working with the left-hand side first,
2 * sum []
= 2 * 0 by (sum.1)
= 0 by *
This shows that the two sides are the same, and so completes the proof of the base
case.
212 CHAPTER 9. REASONING ABOUT PROGRAMS
Here we are required to prove (ind). As in the base case we have the defining equa-
tions of doubleAll and sum, but we also can – and usually should – use the induc-
tion hypothesis (hyp).
We work as we did in the base case, simplifying each side as much as we can
using the defining equations. First the left-hand side,
2 * sum (x:xs)
= 2 * (x + sum xs) by (sum.2)
= 2*x + 2 * sum xs by arith.
Now, we have simplified each side using the defining equations. The last step equat-
ing the two is given by the induction hypothesis (hyp), which can be used to carry
on the simplification of the left-hand side, giving
and so this final step makes the left- and right-hand sides equal, on the assumption
that the induction hypothesis holds. This completes the induction step, and there-
fore the proof itself.
• State clearly the goal of the induction and the two sub-goals of the induction
proof: (base) and (hyp) ) (ind).
• If any confusion is possible, change the names of the variables in the relevant
definitions so that they are different from the variable(s) over which you are
doing the induction.
9.6. FURTHER EXAMPLES OF PROOFS BY INDUCTION 213
• The only resources available are the definitions of the functions involved and
the general rules of arithmetic. Use these to simplify the sub-goals. If the sub-
goal is an equation, then simplify each side separately.
• In the case of the induction step, (ind), you should expect to use the induc-
tion hypothesis (hyp) in your proof; if you do not, then it is most likely that
your proof is incorrect.
• Label each step of your proof with its justification: this is usually one of the
defining equations of a function.
In the next section we look at a series of examples.
Example
1. length and ++
We begin by looking at the example (length.4) introduced at the start of the chap-
ter.
length (xs ++ ys) = length xs + length ys (length.4)
The Quick Check property expressing this is
prop_lengthPlusPlus :: [a] -> [a] -> Bool
prop_lengthPlusPlus xs ys =
length (xs ++ ys) == length xs + length ys
and this is verified when the property is checked. Recall the definitions of length
and ++
length [] = 0 (length.1)
length (z:zs) = 1 + length zs (length.2)
[] ++ zs = zs (++.1)
(w:ws) ++ zs = w:(ws++zs) (++.2)
where we have chosen new names for the variables so as not to conflict with the
variables in the goal.
There is some question about how to proceed with the proof, since (length.4)
involves two variables, xs and ys. We can be guided by the definitions, where we
see that the definition of ++ is made by recursion over the first variable. We therefore
make the goal a proof of (length.4) for all finite xs and ys by induction over xs; the
proof works for all ys as ys is a variable, which stands for an arbitrary list, just like
the variable x in the earlier proof of (length.3) stood for an arbitrary list element.
214 CHAPTER 9. REASONING ABOUT PROGRAMS
Statement We can now write down the two goals of the induction proof. The
base case requires that we prove
length ([] ++ ys) = length [] + length ys (base)
and in the induction step we have to prove
length ((x:xs) ++ ys) = length (x:xs) + length ys (ind)
from the inductive assumption
length (xs ++ ys) = length xs + length ys (hyp)
Base We look separately at the two sides of (base), left-hand side first,
length ([] ++ ys)
= length ys by (++.1)
length [] + length ys
= 0 + length ys by (length.1)
= length ys
which shows their equality.
2. reverse and ++
What happens when we reverse the join of two lists, xs++ys? The process is illus-
trated in FigurereverseSwap. Each list is reversed, and they are swapped. In formal
terms,
reverse (xs ++ ys) = reverse ys ++ reverse xs (reverse++)
where we define
reverse [] = [] (reverse.1)
reverse (z:zs) = reverse zs ++ [z] (reverse.2)
We will try to prove (reverse++) for all finite lists xs and ys by induction over xs.
9.6. FURTHER EXAMPLES OF PROOFS BY INDUCTION 215
xs ys
ys xs
reverse ys ++ reverse []
= reverse ys ++ [] by (reverse.1)
but we can prove the two equal only if we can show that appending an empty list to
the end of a list is an identity operation, that is
xs ++ [] = xs (++.3)
We leave a proof of this by induction over xs as an exercise for the reader.
Induction Again, we look at the two sides of the equation, left-hand side first.
prop_reversePlusPlus xs ys =
reverse (xs ++ ys) == reverse ys ++ reverse xs
we find that it holds. Fine, but if we check this property:
prop_reversePlusPlusOops xs ys =
reverse (xs ++ ys) == reverse xs ++ reverse ys
we find that this holds too!
What is happening here? The problem is that when we check a generic property –
that is one over a polymorphic type – then it is in fact checked over a default type.
This default type is (), which has exactly one element, also (). The incorrect prop-
erty is correct over lists of that type, because all their elements are the same!
In order to restore sanity, it is enough to change the properties to a suitable non-
generic type, using a type declaration like this:
Now, these two are almost equal, except that the joins are bracketed differently. We
need another general property of ++, namely that it is associative:
This proof is instructive: it shows that often in proofs we use other theorems or lem-
mas (the mathematician’s term for a ‘little theorem’) on the way. If we do any seri-
ous proof we will build up a library of these lemmas, with (++.3) and (++.4) being
basic results about ++ which we will call upon almost without thinking. We would
expect this library to resemble the standard prelude: it would contain all those the-
orems which link the prelude functions and which will be called into use whenever
we use prelude functions. Many of the exercises at the end of the section ask you to
prove theorems concerning prelude functions.
Exercises
9.6. FURTHER EXAMPLES OF PROOFS BY INDUCTION 217
9.3 Prove the properties for functions over Picture first discussed in Section 1.14,
that is, for all finite pic:
9.4 Look again at the properties for functions over Picture discussed in Section
6.4, and prove those properties which should hold for all pictures.
xs ++ [] = xs (++.3)
xs ++ (ys ++ zs) = (xs ++ ys) ++ zs (++.4)
unzip [] = ([],[])
unzip ((x,y):ps)
= (x:xs,y:ys)
where
(xs,ys) = unzip ps
218 CHAPTER 9. REASONING ABOUT PROGRAMS
take n xs ++ drop n xs = xs
9.11 Write QuickCheck properties for the propositions that you have proved, and
check that they indeed hold.
shunt [] ys = ys (shunt.1)
shunt (x:xs) ys = shunt xs (x:ys) (shunt.2)
Starting with an empty second argument, we have
shunt [2,3,1] []
; shunt [3,1] [2]
; shunt [1] [3,2]
; shunt [] [1,3,2]
; [1,3,2]
and so we can reverse lists using this function:
Now, by (hyp), where we take the particular value (x:zs) to replace the universally
quantified variable zs,
This is the right-hand side, and so the proof is complete for an arbitrary ys, giving a
proof of (ind), and completing the induction proof.
This example shows that we may have to generalize what has to be proved in order
for induction proofs to work. This seems paradoxical: we are making it harder for
ourselves, apparently. We are in one way, but at the same time we make the induc-
tion hypothesis stronger, so that we have more resources to use when proving the
induction step.
Exercises
we can define
fac2 n = facAux n 1
fac n = fac2 n
9.14 Write a property expressing that the old and new reverse functions have the
same behaviour.
9.15 Write a property expressing that the old and new factorial functions have the
same behaviour. What happens to the QuickCheck of the property if you allow
arbitrary integer inputs to the factorial functions? how can you remedy this?
Summary
This chapter has shown that we can give Haskell programs a logical reading which
allows us to reason about them. Central to reasoning about lists is the principle of
structural induction, which does for proof what primitive recursion does for defini-
tions.
We gave a collection of hints about how we can build proofs for functional pro-
grams, and illustrated these by giving a number of results for common prelude func-
tions such as sum, ++ and length, as well as exercises involving others. We also saw
how QuickCheck could be used to check whether or not a property appears to hold:
this is a very useful way of making a ‘sanity check’ of a property before we try to
prove it.
222 CHAPTER 9. REASONING ABOUT PROGRAMS
Chapter 10
Generalization: patterns of
computation
Software reuse is a major goal of the software industry. One of the great strengths
of modern functional programming languages like Haskell is that we can use them
to define general functions which can be used in many different applications. The
Haskell prelude functions over lists, for instance, form a toolkit to which we turn
again and again in a host of situations.
We have already seen one aspect of this generality in polymorphism, under
which the same program can be used over many different types. The prelude func-
tions over lists introduced in Chapter 5 provide many examples of this including
length, ++ and take.
As we said, these functions have the same effect over every argument – length
computes the length of a list of any type, for instance. In this chapter we explore
a second mechanism, by which we can write functions which embody a pattern of
computation; two examples of what we mean follow.
• Transform every element of a list in some way. We might turn every alphabetic
character into upper case, or double every number.
• Combine the elements of a list using some operator. We could add together the
elements of a numeric list in this way, for example.
How can we write general functions which implement patterns like this? We need
to make the transformation or operator into a parameter of the general function;
in other words we need to have functions as arguments of other functions. These
higher-order functions are the topic of this chapter. Complementing this is the abil-
ity to make functions the results of functions; we look at that in the next chapter.
We begin the chapter by examining the patterns of computation over lists which
we have encountered so far, and in the remaining sections of the chapter we show
how these are realized as higher-order Haskell functions. We also re-examine primi-
tive recursive definitions, and see that they generalize the process of combining the
elements of a list using an operator.
223
224 CHAPTER 10. GENERALIZATION: PATTERNS OF COMPUTATION
• taking the second element of each pair in a list of pairs, as we do in the library
database;
• in the supermarket billing example, converting every item in a list of bar codes
to the corresponding (Name,Price) pair;
• formatting each (Name,Price) pair in a list.
• select each pair which has a particular person as its first element;
• select each pair which is not equal to the loan pair being returned.
10.1. PATTERNS OF COMPUTATION OVER LISTS 225
In a similar way,
• ++ can be folded into a list of lists to concatenate it, as is done in the definition
of concat;
• && can be folded into a list of Booleans to take their conjunction: this is the
prelude function and;
• max can be folded into a list of integers to give their maximum.
Breaking up lists
A common pattern in the text processing example of Chapter 7 is to take or drop
items from a list while they have some property. A first example is getWord,
in which we continue to take characters while they are alphabetic. Other examples
include dropWord, dropSpace and getLine. In the last of these the property in
question depends not only upon the particular list item but also on the part of the
list selected so far.
Combinations
These patterns of definition are often used together. In defining books for the library
database, which returns all the books on loan to a given person, we filter out all
pairs involving the person, and then take all second components of the results. The
strength of list comprehensions is that they give this combination of mapping and
filtering, which fits some examples – like the library database – particularly well.
Other combinations of functions are also common.
• In the pictures case study the function invertColour inverts the colour of
every character in a Picture by inverting every line; inverting a line requires
us to invert every character, so here we have two, nested, uses of mapping.
• Formatting the item part of a supermarket bill involves processing each item
in some way, then combining the results, using ++.
iSort [] = []
iSort (x:xs) = ins x (iSort xs)
Haskell provides a mechanism to turn a prefix function like ins into an infix version.
The name is enclosed by back quotes, ‘ins‘, so
Looked at this way, the definition looks like ‘ins‘ folded into the list [4,2,3]. We
shall look at this again in Section 10.3.
splitLines [] = []
splitLines ws
= getLine lineLen ws
: splitLines (dropLine lineLen ws)
For a non-empty list of words ws, the result splitLines ws is defined using a recur-
sive call of splitLines not on the tail of ws but on (dropLine lineLen ws). This
form of recursion will terminate because (dropLine lineLen ws) will always be
shorter than ws itself, at least in sensible cases where no word in the list ws is longer
than the line length lineLen.
The figure shows how the types of the functions and lists are related, giving map the
type
where recall that a and b are type variables, standing for arbitrary types. Instances
of the type of map include
as used in the definition of doubleAll, where map is applied to the function double
of type Int -> Int and
and we find out whether a particular character like ’d’ is a digit or not by applying
the function to the character to give a Boolean result, that is True or False.
This is the way that we can model a property over any type t. The property is
given by a function of type
t -> Bool
and an element x has the property precisely when f x has the value True. We have
already seen the example of isDigit; other examples include
We usually adopt the convention that the names of properties begin with ‘is’.
filter p [] = [] (filter.1)
filter p (x:xs)
| p x = x : filter p xs (filter.2)
| otherwise = filter p xs (filter.3)
In the case of an empty list, the result is empty. For a non-empty list (x:xs) there are
two cases. If the guard condition p x is true then the element x is the first element
of the result list; the remainder of the result is given by selecting those elements in
xs which have the property p. If p x is False, x is not included, and the result is
given by searching xs for elements with property p.
A list comprehension also serves to define filter,
where again we see that the condition for inclusion of x in the list is that it has the
property p.
Our example digits is defined using filter as follows
What is the type of filter? It takes a property and a list, and returns a list.
which ‘zips together’ the the elements of two lists into a single list of pairs, pairing
up corresponding elements in the two lists. For instance,
As the example shows, if the lists are of different lengths, we just drop the elements
in the longer list with no element to pair with.
What happens if we want to do something to two corresponding elements other
than making a pair of them? Recall from Chapter 1 that in our Picture case study
to define beside we wanted to join corresponding lines using (++). To this end we
define the zipWith function, which combines the effect of zipping and mapping:
Exercises
10.1 Write three line-by-line calculations of doubleAll [2,1,7] using the three
different definitions of doubleAll that use a list comprehension, primitive
recursion and map.
10.2 How would you define the length function using map and sum?
where
greaterOne n = n>1
addOne n = n+1
Can you conclude anything in general about properties of map f (map g xs)
where f and g are arbitrary functions?
10.7 Using functions defined already wherever possible, write definitions of func-
tions to
10.8 State the type of and define a function twice which takes a function from inte-
gers to integers and an input integer, and whose output is the function applied
to the input twice. For instance, with the double function and 7 as input, the
result is 28. What is the most general type of the function you have defined?
where f occurs n times on the right-hand side of the equation. For instance,
we should have
iter 3 f x = f (f (f x))
10.10 Using iter and double define a function which on input n returns 2n ; re-
member that 2n means one multiplied by two n times.
232 CHAPTER 10. GENERALIZATION: PATTERNS OF COMPUTATION
10.11 Define QuickCheck properties that you would expect to hold for the result of
a filter,
filter p xs
g (f x) ; x
f (g y) ; y
for all x and y, give properties that you would expect to hold for the result of
the map:
map f xs
• The first argument is a binary function over the type a; for example, the func-
tion (+) over Int.
10.3. FOLDING AND PRIMITIVE RECURSION 233
• The second is a list of elements of type a which are to be combined using the
operator; for instance, [3,98,1]
The result is a single value of type a; in the running example we have
foldr1 (+) [3,98,1] = 102
Other examples which use foldr1 include
foldr1 (||) [False,True,False] = True
foldr1 (++) ["Freak ", "Out" , "", "!"] = "Freak Out!"
foldr1 min [6] = 6
foldr1 (*) [1 .. 6] = 720
The function foldr1 gives an error when applied to an empty list argument.
We can modify the definition to give an extra argument which is the value re-
turned on the empty list, so giving a function defined on all finite lists. This function
is called foldr and is defined as follows
foldr f s [] = s (foldr.1)
foldr f s (x:xs) = f x (foldr f s xs) (foldr.2)
The ‘r’ in the definition is for ‘fold, bracketing to the right’. Using this slightly more
general function, whose type we predict is
foldr f s [] = s (foldr.1)
foldr f s (x:xs) = f x (foldr f s xs) (foldr.2)
What is the effect of foldr f s? We have two cases:
• the value at (x:xs) is defined in terms of the value at xs, and x itself.
This is just like the definition of primitive recursion over lists in Chapter 7.1 Because
of this it is no accident that we can define many of our primitive recursive functions
using foldr. It is usually mechanical to go from a primitive recursive definition to
the corresponding application of foldr.
How do the two approaches compare? It is often easier initially to think of a
function definition in recursive form and only afterwards to transform it into an ap-
plication of foldr. One of the advantages of making this transformation is that we
might then recognize properties of the function by dint of its being a fold. We look at
proof for general functions like map, filter and foldr in Section 11.6 and we look
at other fold functions in Chapter 20.
Exercises
10.13 How would you define the sum of the squares of the natural numbers 1 to n
using map and foldr?
10.14 Define a function to give the sum of squares of the positive integers in a list of
integers.
1 There is an ambiguity in our original characterization. In defining the function g by primitive recur-
sion the value of g (x:xs) is defined in terms of both x and xs as well as the value g xs itself; this makes
primitive recursion slightly more general than folding using foldr.
10.3. FOLDING AND PRIMITIVE RECURSION 235
10.15 For the purposes of this exercise you should use foldr to give definitions of
the prelude functions unZip, last and init, where examples of the latter two
are given by
10.17 The function formatLines is intended to format a list of lines using the func-
tion
a -> String
to format each item of the list which is passed as the second parameter. Show
how formatLines can be defined using formatList and formatLine.
10.20 How could you define a function switchMap which maps two functions along
a list, alternating which to apply. For example,
236 CHAPTER 10. GENERALIZATION: PATTERNS OF COMPUTATION
where addOne and addTen behave as you would expect. What is the most
general type of switchMap?
so that spilt will split a list into two lists, picking elements alternately, while
merge will interleave the two lists; for example,
10.22 Can you formulate QuickCheck properties which characterise the way that
split and merge work together?
g x (g y z) = g (g x y) z
give a QuickCheck property that you would expect foldr1 g (xs ++ ys) to
have. Can you think of a similar property for foldr g s (xs ++ ys)? Hint:
you will need to think about what property s needs to obey.
where take n xs and drop n xs are intended to take or drop n elements from the
front of the list xs. These functions are defined in Chapter 7.
Also in Chapter 7 we looked at the example of text processing, in which lists were
split to yield words and lines. The functions getWord and dropWord defined there
were not polymorphic, as they were designed to split at whitespace characters.
10.4. GENERALIZING: SPLITTING UP LISTS 237
getWord xs
= getUntil p xs
where
p x = elem x whitespace
Built into Haskell are the functions takeWhile and dropWhile, which are like getUntil
and dropUntil, except that they take or drop elements while the condition is True.
For instance,
Exercises
10.24 Give the type and definition of the generalization dropUntil of the function
dropWord.
10.25 How would you define the function dropSpace using dropUntil? How would
you define takeWhile using getUntil?
238 CHAPTER 10. GENERALIZATION: PATTERNS OF COMPUTATION
10.26 How would you split a string into lines using getUntil and dropUntil?
10.27 The function getLine of Chapter 7 has a polymorphic type – what is it? How
could you generalize the test in this function? If you do this, does the type of
the function become more general? Explain your answer.
10.28 Can you give generalizations to polymorphic higher-order functions of the text
processing functions getLine, dropLine and splitLines?
Pictures
We have discussed the Picture type and the functions over it already in Chapter 1,
Sections 2.6 and 4.2 and Chapter 6. In this chapter we have seen that we can define
flipV using map, and beside using zipWith. The exercises that follow pick up other
examples.
Exercises
10.29 How can you use map to define the invertColour function, which turns a
Picture into its negative?
10.30 How can you use zipWith to define the superimpose function,
which superimposes one Picture (the first argument) on top of another (the
second)? What does your function do if the pictures are not the same size? Can
you modify your definition so that it handles this case properly?
10.31 [Harder] Using map and any other functions that you need, define the function
Exercises
10.33 Re-implement the functions from the previous exercise using this new defini-
tion of the Database type:
10.34 Revisit the exercises of Section 6.7, where the supermarket billing example was
developed, and re-implement your solutions using the prelude and library
functions map, filter and so on.
10.35 In the light of the previous exercises, can you come to any conclusions about
when it is sensible to use list comprehensions, and when it is more useful to
use the prelude and library functions explicitly?
Exercises
10.36 Using the function outcome from Exercise 8.1 (page 184) and standard list
functions such as map, redefine the function
first introduced on page 200, using the standard list functions introduced in
this chapter.
240 CHAPTER 10. GENERALIZATION: PATTERNS OF COMPUTATION
Summary
This chapter has shown how the informal patterns of definition over lists can be
realized as higher-order, polymorphic functions, such as map, filter and foldr.
We saw how these functions arose, and also how their types were derived, as well as
reviewing the ways in which they could be used to solve problems.
Next we looked at an example of how to generalize a function – the particular
example was taken from the text processing case study, but the example serves as
a model for how to generalize functions in general. The chapter concludes with a
re-examination of some of the case studies.
The chapter has focused on how to write functions which take other functions
as arguments; where do these arguments come from? One answer is that they are
already defined; another is that they come themselves as the results of Haskell func-
tions – this is the topic of the next chapter.
Chapter 11
Higher-order functions
Haskell is a functional programming language: that means that the main way in
which we compute things is by defining functions which describe how to transform
the inputs into the required output. Haskell has a collection of built-in data types
which we can use to model the data in the problem domain, including numbers,
booleans, lists, tuples and data types.
Haskell is also functional in a more distinctive way: functions are data in Haskell,
and can be treated just like data of any other type.
• Functions can be combined using operators, just like the numbers can be
combined using the arithmetical operators.
• Functions can be the inputs and outputs of other functions in exactly the same
way as any other type. Functions which have other functions as arguments or
results are called higher-order functions.
This chapter covers these topics, setting the scene for Chapter 12 where will put
these ideas into practice.
Finally, we’ll also see that we can start to write function-level definitions – some-
times called ‘point-free’ definitions – which can be more concise, more readable and
more suitable for program verification and transformation. Indeed, the chapter con-
cludes with some examples of program verification involving higher-order polymor-
phic functions, and we see there that the theorems proved about them are reusable
in exactly the same way that the functions themselves are reusable.
241
242 CHAPTER 11. HIGHER-ORDER FUNCTIONS
f.g
a b c
g f
Function composition: .
Function composition is one of the simplest ways of structuring a program: do a
number of things one after the other: each part can be designed and implemented
separately.
Haskell has the function composition operator over functions built in. The op-
erator, which is denoted by the ‘.’ between the two functions, has the effect of
‘wiring together’ two functions: passing the output of one to the input of another,
and it is . This is pictured in Figure 11.1, where the annotations of the arrows in the
diagram indicate the types of elements involved.
For any functions f and g, the effect of f.g is given by the definition
(f.g) x = f (g x) (comp.1)
Not all pairs of functions can be composed. The output of g, g x, becomes the input
of f, so that the output type of g must equal the input type of f.
Recalling the Picture example, we have already seen a definition of rotate:
since there is an attempt to treat not True as a function to be composed with not.
Such a function needs to have type a -> Bool, whereas it actually has type Bool.
In applying a composition we therefore need to be sure that it is parenthesized like
this:
(not.not) True
which shows that, if we call the first input f and the second g,
It is simple in Haskell to define an operator for composition which takes its ar-
guments in the opposite order to ‘.’, like this:
infixl 9 >.>
g >.> f = f . g (fcomp.1)
This definition has the effect that
it is possible to write:
with the same meaning. Arguably this is a little clearer, and it is shorter! Inci-
dentally you can see from this example that ‘$’ is right associative.
Exercises
11.1 Redefine the function printBill from the supermarket billing exercise in
Section 6.7 so that composition is used. Repeat the exercise using forward
composition, >.>.
(id.f) (f.id) id f
If f is of type Int -> Bool, at what instance of its most general type a -> a
is id used in each case? What type does f have if f id is properly typed?
11.3 Define a function composeList which composes a list of functions into a sin-
gle function. You should give the type of composeList, and explain why the
function has this type. What is the effect of your function on an empty list of
functions?
If f is of type Int -> Bool, at what instance of its most general type a -> a
is id used in each case? What type does f have if f $ id is properly typed?
addOne x = x+1
Haskell gives us a way of writing down an expression that means ‘the function that
adds one to a number’ directly, without having to give it a name. We write
\x -> x+1
which we can read as saying ‘the function that takes x to x+1’, the initial ‘\’ signalling
that it’s a function. So, we can add one to all the numbers in the list [2,3,4] by
writing the expression
mapFuns [] x = []
mapFuns (f:fs) x = f x : mapFuns fs x
but in fact we can use map in making the definition: what we have to do at each
element of the list (remember, each element is a function) is to apply it to x, so
g (f x) (f y)
so the overall effect is to give a function which applies f to each of its (two) argu-
ments before applying g to the results. Again, the definition states this directly:
comp2 sq add 3 4
where add and sq have the obvious definitions.
In general, a lambda abstraction is an anonymous version of the sort of function
we have defined earlier. In other words, the function f defined by
248 CHAPTER 11. HIGHER-ORDER FUNCTIONS
comp2 f g
x f
g g (f x) (f y)
y f
comp2 f g = \x y -> g (f x) (f y)
f x y z = result
and the function
\x y z -> result
have exactly the same effect.
We shall see in the next section that partial application will make many defini-
tions – including some of the functions here – more straightforward. On the other
hand the lambda abstraction is more general, and thus can be used in situations
when a partial application could not.
Exercises
11.7 Using a lambda abstraction, the Boolean function not and the built-in func-
tion elem describe a function of type
which is True only on non-whitespace characters, that is those which are not
elements of the list " \t\n".
f 0 + f 1 + ... + f n
11.3. PARTIAL APPLICATION 249
You should be able to do this using built-in functions, rather than using recur-
sion.
11.9 Given a function f of type a -> b -> c, write down a lambda abstraction
that describes the function of type b -> a -> c which behaves like f but
which takes its arguments in the other order. Pictorially,
11.10 Using the last exercise, or otherwise, give a definition of the function
which reverses the order in which its function argument takes its arguments.
multiply
If we apply the function to two arguments, the result is a number; so that, for in-
stance, multiply 2 3 equals 6.
2
multiply 6
3
2
multiply
From the picture we can see that this represents a function, as there is still one input
arrow to the function awaiting a value. This function will, when given the awaited
argument y, return double its value, namely 2*y.
This is an example of a general phenomenon: any function taking two or more
arguments can be partially applied to one or more arguments. This gives a powerful
way of forming functions as results.
but it is quite possible to write a function level definition like this, and it is
shorter and clearer than the definition with the arguments supplied.
In Section 11.2 we saw the example of addNum,
addNum n = (\m -> n+m)
which when applied to an integer n was intended to return the function which adds
n to its argument. With partial application we have a simpler mechanism, as we can
say
addNum n m = n+m
since when addNum is applied to one argument n it returns the function adding n to
its argument.
11.3. PARTIAL APPLICATION 251
Order of arguments
It is not always possible to make a partial application, since the argument to which
we want to apply the function may not be its first argument. Let’s look at the function
elem ch whitespace
where whitespace is the string " \t\n". We would like to write the function to
test this by partially applying elem to whitespace, but cannot, because this is the
secdon argument rather than the first.
One solution is to define a variant of elem which takes its arguments in the other
order, as in
member xs x = elem x xs
member whitespace
Alternatively, we can write down this function as a lambda abstraction, like this:
(op x) y = y op x
252 CHAPTER 11. HIGHER-ORDER FUNCTIONS
Parentheses in Haskell
The main role of parentheses, ( . . . ), in Haskell is to group items together so that the
system interprets what you have written in the right way. Typical examples include
(x op) y = x op y
When combined with higher-order functions like map, filter and composition,
the notation is both powerful and elegant, enabling us to make a whole lot more
function-level definitions. For example,
filter (>0) . map (+1)
is the function which adds one to each member of a list, and then removes those
elements which are not positive.
Three simple examples are the text processing functions we first looked at in Sec-
tion 7.6:
dropSpace = dropWhile (member whitespace)
dropWord = dropWhile (not . member whitespace)
getWord = takeWhile (not . member whitespace)
where
member xs x = elem x xs
Exercises
11.11 Use partial applications to define the functions comp2 and total given in Sec-
tion 11.2 and its exercises.
11.13 Re-define the function mapFuns, first defined in Section 11.2, using an opera-
tor section of the application operator, $.
(multiply 2) 5 :: Int
which, since function application is left associative, can be written
multiply 2 5 :: Int
Our explanations earlier in the book are consistent with this full explanation of the
system. We hid the fact that
f e1 e2 ... ek
t1 -> t2 -> ... tn -> t
were shorthand for
( ...((f e1 ) e2 ) ... ek )
t1 -> (t2 -> (...(tn -> t)...))
but this did no harm to our understanding of how to use the Haskell language. It is
to support this shorthand that function application is made left associative and ->
is made right associative.
Int
f Int
Int
(Int->Int) g Int
The function f will yield a function from Int to Int when given a Int – an example
is multiply. On the other hand, when given a function of type Int -> Int, g yields
a Int. An example is
(where k∑n) then the result type is given by cancelling the types t1 to tk
/1 -> t
t tk -> tk+1 -> ... -> tn -> t
/2 -> ... -> /
For example, using this rule we can see that we get the following types
while an uncurried version can be given by bundling the arguments into a pair, thus:
Why do we usually opt for the curried form? There are a number of reasons.
We can in any case move between the curried and uncurried representations with
little difficulty, and indeed we can define two higher-order functions which convert
between curried and uncurried functions.
Suppose first that we want to write a curried version of a function g, which is
itself uncurried and of type (a,b) -> c.
curry g
x (x,y)
g g (x,y)
y
This function expects its arguments as a pair, but its curried version, curry g, will
take them separately – we therefore have to form them into a pair before applying g
to them:
uncurry f
x
(x,y) f f x y
y
The function uncurry f will expect its arguments as a pair, and these will have to
be separated before f can be applied to them:
uncurry multiply will be exactly the same function as multiplyUC. The functions
curry and uncurry are inverse to each other.
A disadvantage of the curried representation of functions is that the inverse of a
function like
(or zip’ as we called it earlier in the book) so that statements of properties of these
functions, such as
will necessarily involve the uncurried version of the binary function, rather than the
curried.
Exercises
11.14 What is the effect of uncurry ($)? What is its type? Answer a similiar ques-
tion for uncurry (:), uncurry (.).
11.15 [Harder] What are the effects and types of uncurry uncurry, curry uncurry.
11.16 Can you state a property relating unzip and uncurry zip, where the latter is
the function applied first?
which perform the analogue of curry and uncurry but for three arguments
rather than two? Can you use curry and uncurry in these definitions?
which perform the analogue of curry and uncurry but for a list of arguments
rather than two distinct arguments? Can you use curry and uncurry in these
definitions?
f is a function, and the result is f composed with itself. For this to work, it needs to
have the same input and output type, so we have
This states that twice takes one argument, a function of type (a -> a), and re-
turns a result of the same type. For instance, if succ is the function to add one to an
integer,
(twice succ) 12
; (succ.succ) 12 by (twice.1)
; succ (succ 12) by definition of .
; 14
We can generalize twice so that we pass a parameter giving the number of times the
functional argument is to be composed with itself
iter n f
| n>0 = f . iter (n-1) f (iter.1)
| otherwise = id (iter.2)
This is a standard primitive recursion over the integer argument; in the positive case
we take the composition of f with itself n-1 times and compose once more with f.
In the zero case we apply f no times, so the result is a function which returns its
argument unchanged, namely id.
As an example of using iter, we can define 2n as iter n double 1, if double
doubles its argument.
Exercises
iter 3 double 1
(comp2 succ (*)) 3 4
comp2 sq add 3 4
11.21 Give an alternative “constructive” definition of iter which creates the list of n
copies of f
[f,f,...,f]
and then composes these functions by folding in the operator ‘.’ to give
f . f . ... . f
addNum n = addN
where
addN m = n+m
or a let
addNum n = let
addN m = n+m
in
addN
This gives us a way of defining results which are functions by locally defining that
function and returning it as the result; it has the advantage of not requiring any
more advanced machinery, but the disadvantage of introducing a named definition
of addN which is extraneous to the actual result.
Lambda abstractions
Let’s try to define a function that takes a binary function as argument, and which
returns a binary function that takes its arguments in the opposite order; let’s call it
flip:
flip :: (a -> b -> c) -> (b -> a -> c)
We can use a lambda abstraction to give the result:
Partial application
Partial applications give us a particularly direct way of defining some higher order
functions. Taking the example we have just looked at, we can instead say
flip f x y = f y x
If we apply flip to one argument, we have a function which behaves just as de-
scribed in (flip.1). Similarly, the behaviour of
addNum n = addN
where
addN m = n+m
is given by the simple definition
addNum n m = n+m
partially applied to n.
Examples
We conclude this section by exploring how partial applications and operator sec-
tions can be used to simplify and shorten definitions in a number of other exam-
ples. Often it is possible to avoid giving an explicit function definition if we can use
a partial application to return a function. Revisiting the examples of Chapter 7 we
see that to double all the elements in a list we can write
(==0).(‘mod‘ 2)
This is the composition of two operator sections: first find the remainder on dividing
by two, then check if it is equal to zero. (Why can we not write (‘mod‘ 2 == 0)?)
The filtering function can then be written
Point-free programming
Some function-level, or ‘point-free’, definitions express what the function does with
real clarity: writing flipV = map reverse precisely describes how to flip a pic-
ture in a vertical mirror. However, it is possible to take this approach too far, and
write definitions which it’s very hard to understood. For instance, what does this
function do:
puzzle x y z w = (x y) (z w)
Finally, we use to get GHCi to give its type, like this :type (.)(.) and get
Our final example comes from the list splitting study. We defined
getWord xs
= getUntil p xs
where
p x = elem x whitespace
The local definition is not now needed, as we can define the function p by an opera-
tor section:
Note the way that we partially apply a function to its second argument, by forming
an operator section. This works because
(‘elem‘ whitespace) x
= x ‘elem‘ whitespace
= elem x whitespace
as required.
Finally, the function getWord can itself be given a direct definition, by partial
application thus
This definition reads like an informal explanation – to get a word, get characters until
a whitespace character is found.
Exercises
which takes a function f as argument, and returns (an approximation to) its
derivative f’ as result.
integrate :: (Float -> Float) -> (Float -> Float -> Float)
which takes a function f as argument, and returns (an approximation to) the
two argument function which gives the area under its graph between two end
points as its result.
Function-level verification
We claimed in Section 11.5 that the function iter is a generalization of twice, since
iter 2 f
= f . iter 1 f by (iter.1)
= f . (f . iter 0 f) by (iter.1)
= f . (f . id) by (iter.2)
= f . f by (compId)
= twice f by (twice.1)
f . id = f (compId)
How is this proved? We examine how each side behaves on an arbitrary argument x
(f . id) x
= f (id x)
= f x
so that for any argument x the two functions have the same behaviour. As black
boxes, they are therefore the same. As what interests us here is their behaviour, we
say that they are equal. We call this ‘black-box’ concept of equality extensional.
Exercises
11.25 Using the principle of extensionality, show that function composition is asso-
ciative: that is, for all f, g and h,
f . (g . h) = (f . g) . h
id . f = f
Verification and general functions 265
11.27 Show that the function flip defined in Section 11.4 satisfies
flip . flip = id
Hint: to show this, you might want to prove that for any f,
flip (flip f) = f
f . g = id g . f = id
Prove that the functions curry and uncurry of Section 11.4 are inverses. Can
you think of other pairs of inverse functions?
iter n id = id
f . f = f
Show that the functions abs and signum are idempotent. Can you think of
any other idempotent functions?
Higher-level proofs
Our verification thus far has concentrated on first-order, monomorphic functions.
Just as map, filter and fold generalize patterns of definition, we shall find that
proofs about these functions generalize results we have seen already. To give some
examples, it is not hard to prove that
holds for all finite lists xs and ys. When doubleAll is defined as map (*2) it be-
comes clear that we have an example of a general result,
which is valid for any function f. We also claimed in an earlier exercise that
for all finite lists xs, ys. The function sum is given by folding in (+),
map f [] = [] (map.1)
map f (x:xs) = f x : map f xs (map.2)
(f . g) x = f (g x) (comp.1)
It is not hard to see that we should be able to prove that
f.g
map (f . g) [] = [] by (map.1)
(map f . map g) []
= map f (map g []) by (comp.1)
= map f [] by (map.1)
= [] by (map.1)
Assuming that
Verification and general functions 267
map (f . g) (x:xs)
= (f . g) x : map (f . g) xs by (map.2)
= f (g x) : map (f . g) xs by (comp.1)
The induction hypothesis is exactly what is needed to prove the two sides equal,
completing the proof of the induction step and the proof itself.
Each Haskell list type, besides containing finite lists, also contains infinite and
partial lists. In Chapter 17 these will be explained and it will be shown that (map.3)
is true for all lists xs, and therefore that the functional equation
holds in general.
The equation says that a map followed by a filter can be replaced by a filter fol-
lowed by a map. The right-hand side is potentially more efficient than the left, since
the map operation will there be applied to a shorter list, consisting of just those ele-
ments with the property (p . f). An example is given by the function first defined
in Section 11.3.
filter p [] = [] (filter.1)
filter p (x:xs)
| p x = x : filter p xs (filter.2)
| otherwise = filter p xs (filter.3)
(f . g) x = f (g x) (comp.1)
The base case consists of a proof of
(filter p . map f) []
= filter p (map f []) by (comp.1)
= filter p [] by (map.1)
= [] by (filter.1)
and
There are two3 cases to consider: whether p (f x) is True or False. Taking the
case where p (f x) is True, we continue to examine the left-hand side of (ind),
giving
= f x : filter p (map f xs) by (filter.2)
= f x : (filter p . map f) xs by (comp.1)
= f x : (map f . filter (p . f)) xs by (hyp)
Now we look at the right-hand side of (ind), also assuming that p (f x) is True:
(map f . filter (p . f)) (x:xs)
= map f (filter (p . f) (x:xs)) by (comp.1)
= map f (x: (filter (p . f) xs)) by (filter.2)
= f x : map f (filter (p . f) xs) by (map.2)
= f x : (map f . filter (p . f)) xs by (comp.1)
which shows that (ind) holds in the case that p (f x) is True.
A similar chain of reasoning gives the same result in the case where p (f x) is
False. This establishes (ind) assuming (hyp), and so together with (base) com-
pletes the proof of the filter promotion transformation in the case of finite lists; it
holds, in fact, for all lists.
• map reverse affects each of the elements, while keeping their order the same.
The second observation is a consequence of the function being a map, and so we
make the more general claim that for all finite lists xs and all functions f,
map f (reverse xs) = reverse (map f xs) (map/reverse)
This has the consequence that
flipV (flipH xs) = flipH (flipV xs)
if we replace f in (map/reverse) by reverse. We will see in Chapter 17 that we
can establish (map/reverse) for all lists xs and so conclude that the functional
equations hold:
3 We should also think about what happens when p (f x) is undefined; in this case both sides will be
undefined, and so equal.
270 CHAPTER 11. HIGHER-ORDER FUNCTIONS
prop_mf p f =
\xs -> (filter p . map f) xs == (map f . filter (p . f)) xs
We can check this for specific values of p and f like this
reverse [] = [] (reverse.1)
reverse (z:zs) = reverse zs ++ [z] (reverse.2)
Base Looking at the two sides of the base case in turn, we have
and this shows that the two sides of the base case equation have the same value, and
so we move on to the induction case.
and now we see that the two sides are equal, which establishes the induction step
and so completes the proof.
Libraries of theorems
We have seen in this section that we can prove properties of general functions like
map, filter and foldr. This means that when we define a function which uses map,
say, we can call on a whole library of properties of map, including, for all finite xs and
ys:
map (f . g) xs = (map f . map g) xs
(filter p . map f) xs = (map f . filter (p . f)) xs
map f (reverse xs) = reverse (map f xs)
map f (ys++zs) = map f ys ++ map f zs
272 CHAPTER 11. HIGHER-ORDER FUNCTIONS
We have seen that using the general functions map, filter and others allowed us
to make direct definitions of new functions rather than having to define them ‘from
scratch’ using recursion. In exactly the same way, these general theorems will mean
that in many cases we can avoid writing an induction proof about our specific func-
tion, and instead simply use one of these theorems.
Exercises
as was used in the proof of the theorem about map and reverse.
11.34 Prove that for all finite lists xs, and functions f,
Summary
We have seen in this chapter how we can write functions with functions as results.
This means that we can create the functions by applying operations like map, filter
and foldr within our programs, and that we can indeed treat functions as ‘first-
class citizens’ of our programming language. A consequence of this has been that
we are able to explain the definitions of some of the Picture operations first seen
in Chapter 1.
The main mechanisms introduced here have allowed us to create functions by
applying functions or operators to fewer arguments than we expected, thus creating
partial applications and operator sections. We also saw how the Haskell-type system
and syntax were adapted to deal with the curried form of function definitions, by
which multi-argument functions take their arguments one at a time.
We concluded by showing that we could prove general properties about general
functions like map, and thus build up libraries of results about these functions which
can potentially be applied whenever the general function is reused.
274 CHAPTER 11. HIGHER-ORDER FUNCTIONS
Chapter 12
Developing higher-order
programs
This chapter doesn’t introduce any new Haskell features; instead it gives a series of
examples, exercises and case studies which build on what we have covered so far. In
particular it uses what we have learned about higher-order functions in the previous
chapter.
As we saw there, functions in Haskell are data values just like any other, and
so they can be used in modelling just as easily as using other data types. This is
completely different from other kinds of programming languages, such as Java, C
or C#, where there is a rigid distinction between data and the methods operating
over that data. We’ll see that having functions as data gives us a powerful new tool
for programming, and that is illustrated in this chapter through a series of examples
and exercises.
We also include in this chapter a longer example – building an index for a doc-
ument – which shows how program development can proceed in Haskell, using
higher-order functions as a natural part of the development of larger programs. We
start the chapter by revisiting the Picture example, and conclude with some gen-
eral advice about program development, and about how to read and understand an
unfamiliar function definition in Haskell.
275
276 CHAPTER 12. DEVELOPING HIGHER-ORDER PROGRAMS
To reflect in a vertical mirror we need to reverse every line – clearly a task for map:
To place pictures next to each other we have two functions. To put one picture above
the other we join together the two lists of lines
while placing the pictures side-by-side requires corresponding lines to be joined to-
gether with ++, using the function zipWith first introduced in Section 10.2.
and we give their definitions now. To invert the colour in a picture, we need to invert
the colour in every line, so
where ... will be the function to invert the colour in a single line. To invert every
character in a line – which is itself a list of characters – we will again use map. The
function mapped is invertChar, first defined in Section 6.4. This gives the defini-
tion
apply map invertChar to every line in the Picture; that is, apply the
function invertChar to every character in the Picture, which is a list
of lists of characters.
which superimposes two characters; how are we to use this in superimposing two
pictures? Recall the function
Exercises
where the list argument gives the positions of the black points in the picture,
and the two integer arguments give the width and height of the picture. For
example,
makePicture 7 5 [(1,3),(3,2)]
.......
...#...
.......
..#....
.......
It is evident from this that positions within lines and lines themselves are counted
from zero, with line zero being the top line.
....
.##.
....
discuss how you would define functions over Rep to rotate, reflect and super-
impose pictures under this alternative representation. Discuss the advantages
and disadvantages of this representation in comparison with the original rep-
resentation given by the Picture type.
12.6 In the light of the discussion in the last four chapters, redo the exercises of
Section 6.6, which deal with positioned pictures.
12.2. FUNCTIONS AS DATA: STRATEGY COMBINATORS 279
Why ‘combinator’?
Why do we use the word ‘combinator’? It’s a piece of history that certain functions
in the ∏-calculus were called combinators, and this usage has passed over to the
functional programming community. Just remember that ‘combinator’ is another
word for ‘function’, typically a higher-order function. A ‘combinator library’ is just a
library of (higher-order) functions, too.
In this definition both of the strategies – put into a list – are applied to the moves and
then one of the elements of that list is chosen using (length moves ‘rem‘ 2) as
an index into the list. In reading this definition, recall that xs!!j is the jth element
of xs, starting counting at 0, and that ($ moves) is a partial application of $, the
application operator, so that
Exercises
12.7 Using randInt n, which returns a random integer in the range [0 .. n-1],
or otherwise, define a function
so that sToss srt1 srt2 makes a random choice between the two strategies
each time the function is applied to a list of moves.
which cycles through all the strategies in the argument in turn. Hint: you may
want to model you definition on one of the definitions of alternative above.
You may also want to think about what your function does when passed the
empty list of strategies!
which makes a random choice of which strategy it should apply form its argu-
ment list, each time it applied. In writing the definition, you will need to think
about what sTossList [] should be.
12.10 Can you use the function sTossList to give another definition of the randomStrategy
strategy first defined in Section 8.1?
We will look at some other suggested strategy combinators in the following exercises.
Exercises
which works by applying all the strategies in the list at each stage, choosing
the option that is chosen by the most strategies: in the case of a draw, a choice
is made randomly.
which is supplied with a list of opponent’s moves to train it, and a list of pos-
sible strategies to use. The function should run all the strategies in the list on
the list of moves, and choose the strategy which is most successful in winning
against the given moves.
• Regular expressions can also be used to extend the pattern language in a pro-
gramming language, allowing functions to match in more powerful ways than
those built in.
(r1 |r2 ) The string st will match (r1 |r2 ) if st matches either r1 or r2 (or
both).
(r1 r2 ) The string st will match (r1 r2 ) if st can be split into two sub-
strings st1 and st2 , st = st1 ++st2 , so that st1 matches r1 and st2
matches r2 .
(r)* The string st will match (r)* if st can be split into zero or more sub-
strings, st = st1 ++st2 ++...++stn , each of which matches r. The
zero case implies that the empty string will match (r)* for any regular
expression r.
Let’s build a model of regular expressions in Haskell; we choose to embed them as
functions from String to Bool, which is the function which recognises exactly the
strings matching the pattern.
epsilon :: RegExp
epsilon = (=="")
We use a similar definition for the function that recognises a single character, passed
in as its argument
e1 ||| e2 =
\x -> e1 x || e2 x
Sequencing the match of two regular expressions is given by the Haskell <*> oper-
ator. In defining this we’ll use the function splits that returns a list of all the ways
that a string can be split in two
e1 <*> e2 =
\x -> or [ e1 y && e2 z | (y,z) <- splits x ]
How does this definition work? The list comprehension runs through all the splits of
the input string, x. For each of these we test whether the front half (y) matches the
first pattern (e1) by applying e1 to x, and similarly we apply e2 to the second half of
the string (z). Since we need both matches to succeed, we combine the results with
‘and’, &&. The result of this is to give a list of the answers for each split: we only need
one of these to succeed, and so we combine the results with the built-in function or
that takes the ‘or’ of a list of Boolean values.
We can define the star operation using the operators that we’ve already defined,
like this:
The definition says ’to match (p)*, either match it zero times (epsilon) or match p
followed by (p)*’. What is elegant about this is that we just used the operators |||
and <*>, together with recursion, to make the definition at the level of the RegExp
type; we didn’t need to think about star p being a function.
where <**> is defined like <*> except that it omits the split ("",st) from splits
st. This change is enough to make sure that the infinite loop is avoided, as it means
that the first match of p can’t be with an empty string, and so the next match of (p)*
must be on a shorter string.
284 CHAPTER 12. DEVELOPING HIGHER-ORDER PROGRAMS
Exercises
which defines the list of all the ways that a list can be split in two (see the
example of splits "Spy" above).
12.14 By trying it with a number of examples, which strings does this regular expres-
sion match?
a, b :: RegExp
a = char ’a’
b = char ’b’
where option e matches zero or one occurrences of the pattern e, and plus
e matches one or more occurrences of the pattern e.
12.17 Define regular expressions which match
so that, for example, range ’A’ ’Z’ will match any capital letter.
12.19 [Hard] Add to the regular expressions the facility to name substrings that match
particular sub-expressions, so that instead of returning a Bool a RegExp will
return a list of bindings of names to substrings.
Why a list? First, it allows for no matching to happen (empty list, []) or for
multiple matches, which can also happen as matching the regular expressions
(r1 r2 ) and (r)* can succeed in multiple different ways.
zero f = id
one f = f
two f = f.f
and so on. We can get a visible version of one of the numbers using the function
Exercises
12.21 Can you write QuickCheck properties which can be used to test these func-
tions and this representation of natural numbers?
Graphics as functions
Bitmaps represent graphical images, recording information pixel by pixel, typically
for a rectangular region. Dealing with real bitmap formats, such as BMP, GIF, TIFF
and others, requires grappling with the details of the encoding and compression
used to store images compactly. Many of these formats are supported in the pack-
ages on Hackage, and as an extended exercise it is possible to transform the repre-
sentation we discuss here into a real graphical format. The remainder of this section
develops this “lo fi” model through a series of exercises.
Representation
We should think how to model Pictures in this way. If we use our previous represen-
tation of positions,
where the first coordinate is the x or horizontal coordinate and the second is the y
or vertical one. We can then think of a bitmap being defined like this:
where Pixel contains the particular information held about an individual pixel.
One “lo fi” model of this – consistent with the Picture type – is to define
but more complex representations of each pixel are possible. The representation
(Bitmap.1) is an infinite bitmap, and to represent finite objects we need to specify
the area that is represented.
• Alternatively, we can specify two positions which give the bottom left and top
right corners of the relevant area of the mapping. This is the positioned rep-
resentation.
Exercises
12.22 Give new definitions of Bitmap which embody the floating and positioned
representations outlined above.
12.5. EXAMPLE: CREATING AN INDEX 287
12.23 Define functions that will convert between your definitions of Bitmap and
Picture.
Operations
We have already discussed pictures in Chapter 1 and then in Sections 2.6, 6.4 and
12.1, and these sections present various operations over the Picture type.
Exercises
12.25 Try to re-implement the operations over pictures using the two representa-
tions of Bitmap you developed earlier. Is it possible to implement all the op-
erations: if not, explain why not.
Taking it further
Exercises
12.27 Develop a variant of Bitmap that allows for pixels to be coloured. You can
render this to a terminal using the facilities of the package ansi-terminal,
which is available on Hackage.
12.28 [Hard] Transform the “lo fi” representation we discussed in this section into a
real graphical format such as BMP, GIF or TIFF, using the facilities provided in
Hackage.
To make the example texts shorter, a scaled-down version of the indexing prob-
lem is investigated. This is only done for ease of presentation, as all the important
aspects of the system are explored here.
Specification
We should first specify what the program is to do. The input is a text string, in which
lines are separated by the newline character ’\n’. The index should give every line
on which the word in question occurs. Only words of length at least four letters are
to be indexed, and an alphabetical listing of the results produced. Within each entry,
a line number should not be duplicated. For example, on the input
battery 2
cathedral 1, 2, 3
doggerel 1, 2
to distinguish the different uses of the string type in the design which follows. Note
that these are all the same type; we use the names to make our discussion of types
carry more information: the definition of ‘Line’ can be read as saying ‘String thought
of as representing a line’, for example.
How can the program be designed? We focus on the data structures which the
program will produce, and we can see the program as working by making a series of
modifications to the data with which we begin. This data-directed design is com-
mon in Haskell functional program development.
At the top level, the solution will be a composition of functions. These perform
the following operations, in turn.
• Split the text, a Doc, into lines, giving an object of type [Line].
• Pair each line with its line number, giving an object of type [(Int,Line)].
12.5. EXAMPLE: CREATING AN INDEX 289
• Split the lines into words, associating each word with the number of the line
on which it occurs. This gives a list of type [(Int,Word)].
• Sort this list according to the alphabetical ordering of words (Strings), giving
a list of the same type.
• Modify the lists so that each word is paired with a list containing a single line
number. This gives a result of type [([Int],Word)].
• Amalgamate entries for the same word into a list of numbers, giving a list of
type [([Int],Word)].
• Shorten the list by removing all entries for words of less than four letters, giving
a list of type [([Int],Word)].
The definition follows; note that we have used comments to give the type of each
component function in the forward composition:
makeIndex
= lines >.> -- Doc -> [Line]
numLines >.> -- [Line] -> [(Int,Line)]
allNumWords >.> -- [(Int,Line)] -> [(Int,Word)]
sortLs >.> -- [(Int,Word)] -> [(Int,Word)]
makeLists >.> -- [(Int,Word)] -> [([Int],Word)]
amalgamate >.> -- [([Int],Word)] -> [([Int],Word)]
shorten -- [([Int],Word)] -> [([Int],Word)]
Once the type of each of the functions is given, development of each can proceed
independently. The only information necessary to use a function is its type, and
these types are specified in the definition above. Each of the functions can now be
given, in turn.
The next function should pair each line with its line number. If the list of lines is
linels, then the list of line numbers is
[1 .. length linels]
Stepping back from the problem, it is apparent that the lists of lines and line num-
bers need to be combined into a list of pairs, by zipping the two lists together. The
zip function has already been defined to do exactly this, so the required function is
290 CHAPTER 12. DEVELOPING HIGHER-ORDER PROGRAMS
whitespace :: String
whitespace = " \n\t;:.,\’\"!?()-"
Each of these words is then to be paired with the (same) line number. Stepping back
from the problem, we see that we have to perform an operation on every item of a
list, the list of words making up the line. This is a job for map,
What has been achieved so far? The text has been transformed into a list of line-
number/word pairs, from which an index is to be built. For instance, the text
The words are compared for dictionary order. For pairs containing the same words,
ordering is by line number.
Sorting a list is most easily done by a version of the quicksort algorithm. The list
is split into parts smaller than and larger than a given element; each of these halves
can be sorted separately, and then joined together to form the result.
sortLs [] = []
sortLs (p:ps) = sortLs smaller ++ [p] ++ sortLs larger
The lists smaller and larger are the lists of elements of ps which are smaller (or
larger) than the pair p. Note that it is here that duplicate copies are removed – any
other occurrence of the pair p in the list ps does not appear in either smaller or
larger.
How are the two lists defined? They are given by selecting those elements of ps
with given properties: a job for filter, or a list comprehension. Going back to the
definition of sortLs,
sortLs (p:ps)
= sortLs smaller ++ [p] ++ sortLs larger
where
smaller = [ q | q<-ps , orderPair q p ]
larger = [ q | q<-ps , orderPair p q ]
After sorting the running example will be
[(2,"bat") , (1,"cat") , (3,"cat") , (1,"dog") , (2,"dog")]
The entries for the same word need to be accumulated together. First each entry is
converted to having a list of line numbers associated with it, thus
makeLists :: [ (Int,Word) ] -> [ ([Int],Word) ]
makeLists
= map mklis
where
mklis ( n , st ) = ( [n] , st )
For our example, this gives
[ ([2],"bat") , ([1],"cat") , ([3],"cat") ,
([1],"dog") , ([2],"dog") ]
After this, the lists associated with the same words are amalgamated.
amalgamate :: [ ([Int],Word) ] -> [ ([Int],Word) ]
amalgamate [] = []
amalgamate [p] = [p]
amalgamate ((l1,w1):(l2,w2):rest)
| w1 /= w2 = (l1,w1) : amalgamate ((l2,w2):rest) (amalg.1)
| otherwise = amalgamate ((l1++l2,w1):rest) (amalg.2)
292 CHAPTER 12. DEVELOPING HIGHER-ORDER PROGRAMS
The first two equations are simple, with the third doing the work.
• If we have two adjacent entries with different words, case (amalg.1), then
we know that there is nothing to add to the first entry – we therefore have to
amalgamate entries in the tail only.
• If two adjacent entries have the same word associated, case (amalg.2), they
are amalgamated and the function is called again on the result. This is because
there may be other entries with the same word, also to be amalgamated into
the leading entry.
Consider an example
To meet the requirements, one other operation needs to be performed. ‘Small’ words
of less than four letters are to be removed.
shorten
= filter sizer
where
sizer (nl,wd) = length wd > 3
Again, the filter function proves useful. The index function can now be defined in
full:
As was said at the beginning of this section, function composition provides a power-
ful method for structuring designs: programs are written as a pipeline of operations,
passing the appropriate data structures between them.
It is easy to see how designs like these can be modified. To take one example, the
indexing program above filters out short words only as its final operation. There are
a number of earlier points in the chain at which this could have been done, and it is
a worthwhile exercise to consider these.
Exercises
12.5. EXAMPLE: CREATING AN INDEX 293
12.30 Define the function lines using the functions getUntil and dropUntil from
Chapter 10, or the built-in functions takeWhile and dropWhile. You should
be careful that your functions do not give an empty word when there are empty
lines in the Doc; this might happen for the examples "cat\n\ndog" and "fish\n".
12.31 How would you use lambda expressions to replace the local definitions in
makeLists and shorten? How would you define these functions using list
comprehensions?
12.32 In the index for this book, instead of printing an entry like
cathedral 3, 5, 6, 7, 9, 10
How would you redesign your program to do this? Hint: first think about
the type of the new index representation and then consider adding another
function to the (forward) composition which currently forms the definition of
makeIndex.
12.33 How would you re-define sortLs so that duplicate copies of an item are not
removed? For the index, this means that if a word occurs twice on line 123 say,
then 123 occurs twice in the index entry for that word.
12.34 How could the functions getUntil and dropUntil be used in the definition
of amalgamate?
12.35 Explain how the function sizer defined locally in shorten can be defined as
a composition of built-in functions and operator sections; the role of sizer is
to pick the second half of a pair, find its length, and compare the result with 4.
12.36 How is the following definition of the last conditional equation for amalgamate
incorrect? Give an example calculation to justify your answer.
amalgamate ((l1,w1):(l2,w2):rest)
| w1 /= w2 = (l1,w1) : amalgamate ((l2,w2):rest)
| otherwise = (l1++l2,w1) : amalgamate rest
12.38 Modify the program so that words of less than four letters are removed as a
part of the definition of allNumWords.
12.39 Modify the makeIndex function so that instead of returning the list of line
numbers on which a word occurs, the function returns the total number of
times that the word occurs. You will need to make sure that multiple occur-
rences of a word in a single line are counted. There are two ways of tackling
the problem.
• Modify the program as little as is necessary – you could return the length of
a list rather than the list itself, for instance.
• Take the program structure as a guide, and write a (simpler) program which
calculates the number of occurrences directly.
12.40 Modify the program so that capitalized words like "Dog" are indexed under
their uncapitalized equivalents ("dog"). This does not work well for proper
names like "Amelia" — what could you do about that?
12.41 The function sortLs is limited to sorting lists of type [(Int,Word)] because
it calls the orderPair function. Redefine the function so that it takes the com-
parison function as a parameter. What is its type after this redefinition?
12.42 How would you modify the program if it was to be used to form the index for
a Haskell script? Hint: you need to think about what it is sensible to ignore in
such an enterprise.
[1 .. n] = 1 : [2 .. n] (..1)
but the problem here is that [2 .. n] is not an instance of what we are trying to
define. The presence of the 2 here suggests that instead of solving the particular
problem of lists starting at 1 we should solve the more general problem of defining
lists beginning at an arbitrary value. We therefore define [m .. n]:
[m .. n]
| m>n = [] (..2)
| otherwise = m : [m+1 .. n]
12.6. DEVELOPMENT IN PRACTICE 295
[1 .. n]
| 1>n = [] (..3)
| otherwise = [1 .. n-1] ++ [n]
but (..3) has the disadvantage that it is substantially less efficient than (..2), a
topic we pick up in Chapter 20.
Another example of generalization was given in the text processing example in
Section 7.6 where we defined a function getLine. The effect of this function is to
take a list of words and to return the list of words making up the maximal first line
(of length lineLen) which can be built from the words. It was apparent in making
the definition that we needed to make the line length a parameter of the definition,
so that we defined
Design choices
The clean function combines mapping (capitals to smalls) and filtering (removing
punctuation) and so can be solved thus
296 CHAPTER 12. DEVELOPING HIGHER-ORDER PROGRAMS
Auxiliary functions
Suppose we are asked to define when one string is a subsequence of another. By
that we mean that the characters of the first string occur next to each other inside
the second string, so that "Chip" is a subsequence of "Fish & Chips", but not of
"Chin up". The function we seek to define is
and we try to define this by recursion. Starting with the cases of the empty string,
subseq [] _ = True
subseq (_:_) [] = False
This latter is not a recursive call to the function we are defining, so we have to say
Exercises
[m,n .. p]
12.7. UNDERSTANDING PROGRAMS 297
so that the result of subst start find replace is the string start modi-
fied so that the first occurrence of find as a subsequence is replaced by replace.
If there is no such subsequence, the string should be returned unmodified, so
that, for instance,
mapWhile f p [] = [] (mapWhile.1)
mapWhile f p (x:xs)
| p x = f x : mapWhile f p xs (mapWhile.2)
| otherwise = [] (mapWhile.3)
we can understand what it means in various different ways. We can read the pro-
gram itself, we can write calculations of examples using the program, we can prove
properties of the program, and we can estimate its space and time complexity,
In the definition we have a complete description of how the program behaves, but
we can animate this by trying specific examples.
(Int -> Int) -> (Int -> Bool) -> [Int] -> [Int]
of its polymorphic type, given by replacing the type variables a and b by the type
Int.
Program behaviour
It is not hard to see that the program will at worst take time linear (that is O(n1 )) in
the length (n) of the list argument assuming O(n0 ) behaviour of f and p, as it runs
through the elements of the list once, if at all.
The space behaviour is more interesting; because we can output the head of a
list once produced, the space required will be constant, as suggested by underlining
the parts which can be output in the calculation above.
Getting started
Each view of the program gives us a different understanding of its behaviour, but
when we are presented with an unfamiliar definition we can begin to understand
what its effect is by calculating various small examples. If we are given a collection
of functions, we can test out the functions from the bottom up, building one calcu-
lation on top of another, using GHCi as a calculator.
The important thing is to realize that rather than being stuck, we can get started
by calculating representative examples to show us the way.
Summary
This chapter has explored the idea that program development works in a cycle: first
we clarify the specification of the problem to be solved, next we devise a plan of how
to solve the problem, and only then do we implement the solution.
At each stage we should reflect on and evaluate what we have done: this aspect
is crucial particularly when we are learning to program. For example, being aware
of the errors that we make can help us to prevent making them in the future. Also,
300 CHAPTER 12. DEVELOPING HIGHER-ORDER PROGRAMS
if we take a problem we have already solved and try to solve it with a new technique
we will learn something about the new technique as well as seeing how it fits in with
what we have learned already. This is something that we do by continually revisiting
the Picture case study.
Chapter 13
In looking at Haskell so far we have seen two kinds of function which work over more
than one type. A polymorphic function such as length has a single definition which
works over all its types. On the other hand, overloaded functions like equality, + and
show can be used at a variety of types, but with different definitions at the different
types.
The chapter starts with a discussion of the benefits of overloading, before looking
at type classes, which are collections of types; what the members of a class have in
common is the fact that certain functions are defined over the type. For instance,
the members of the equality type class, Eq, are those types which carry an equality
function, ==. Type classes are thus the mechanism by which overloaded functions
can be given types in Haskell.
We shall see how to define type classes and types which belong to these classes:
the instances of the class. Haskell’s prelude and libraries contain a number of classes
and instances, particularly for numeric types – we survey these, referring readers to
the Haskell report (Marlow 2010) for a full exposition.
We then look at how type inference and type checking work in Haskell, first look-
ing at types without type variables – monomorphic definitions – and then at poly-
morphic, overloaded definitions, and see how they are given types, illustrated by a
series of examples.
301
302 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
where we have written ==Bool for the equality function over Bool.
Suppose now that we want to check whether an integer is a member of an integer
list, then we need to define a new function
which differs from elemBool only in using ==Int instead of ==Bool . Each time
we want to check membership of a list of a different type we will have to define
yet another – very similar – function. One way out of this problem is to make the
equality function a parameter of a general function
but this gives too much generality, because it can be used with any parameter of
type a -> a -> Bool rather than just an equality function. More importantly, us-
ing this definition the parameter has to be passed in explicitly each time the function
elemGen is used, like this
elemGen (==Bool )
making programs less easy to read.1 The alternative is to define a function which
uses the overloaded equality,
where the type a has to be restricted to those types a which have an equality. The
advantages of this approach are
• Reuse The definition of elem can be used over all types with equality.
• Readability It is much easier to read == than ==Int and so on. This argu-
ment holds particularly for numeric operators, where it is more than tiresome
to have to write +Int , *Float and so on.
What this discussion shows is that a mechanism is needed to give a type to functions
like elem: that is precisely the purpose of type classes.
but elem only has this type for types a which have an equality function. How is this
to be expressed? We need some way of saying whether we have an equality function
over a given type. We call the collection of types over which a function is defined a
type class or simply a class. For instance, the set of types over which == is defined is
the equality class, Eq.
‘Instances’ in Haskell
It is unfortunate that the term instance is used in two different ways in Haskell. We
talked in Section 6.1 of a type t1 being an instance of a type t2 , when we can sub-
stitute for a type variable in t2 to give t1 . Here we have talked about a type being
an instance of a class. It should be clear which one we mean from the context of a
discussion, but it’s helpful to bear this ‘overloading’ of terminology in mind.
The part before the => is called the context. We can read the type as saying that
allEqual has type a -> a -> a -> Bool for all types a in the class Eq
(that is, all types a for which == is defined)
This means that allEqual can be used at many types, including these
which conveys the fact that (Integer -> Integer) is not in the Eq class, and sug-
gests that the way to fix the problem is to add an instance declaration for that type.
and so on. Many of the functions we have defined already use equality in an over-
loaded way. We can use GHCi to deduce the most general type of a function, such as
the books function from the library database of Section 5.7, by commenting out its
type declaration in the script, thus
13.2. INTRODUCING CLASSES 305
:type books
to the prompt. The result we get in that case is
lookupFirst ws x
= [ z | (y,z) <- ws , y==x ]
Clearly from this definition there is nothing specific about books or people and so it
is polymorphic, if we can compare objects in the first halves of the pairs for equality.
This condition gives rise to the context Eq a. Finally from Section 5.7, as we saw for
books,
borrowed :: Eq b => [ (a,b) ] -> b -> Bool
numBorrowed :: Eq a => [ (a,b) ] -> a -> Int
Summary
In this section we have introduced the idea of a class, which is a collection of types,
its instances, with the property that certain functions described in an interface are
defined over the type. One way we can think of a class is as an adjective: any partic-
ular type is or is not in the class, just as the weather at any particular moment might
or might not be sunny.
We saw how equality could be seen as being defined over all the types in the
class Eq. This allows many of the functions defined so far to be given polymorphic
type, allowing them to be used over any type in the class Eq. In the following sec-
tions we explain how classes and instances are defined in general, and explore the
consequences of classes for programming in Haskell.
Exercises
13.1 How would you define the ‘not equal’ operation, /=, from equality, ==? What
is the type of /=?
13.2 Define the function numEqual which takes a list of items, xs say, and an item,
x say, and returns the number of times x occurs in xs. What is the type of your
function? How could you use numEqual to define member?
306 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
oneLookupFirst takes a list of pairs and an item, and returns the second part
of the first pair whose first part equals the item. You should explain what your
function does if there is no such pair. oneLookupSecond returns the first pair
with the roles of first and second reversed.
Declaring a class
As we saw earlier, a class is introduced by a declaration like:
The declaration introduces the name of the class, Info, and this is followed by an
interface or signature, that is a list of identifiers and their types. To be in the Info
class the type a must carry the two bindings in the signature:
• the examples list, which is a list of objects of type a, which we can think of as
giving a list of representative examples from the type, and,
• the size function, which returns a measure of the size of the argument, as an
integer.
Suppose that the type a is in the Info classs: this means that we can estimate the
size of a value in a, and we have a list of examples of type a. Using this function and
list we are able to define a size function for [a] and a list of examples of type [a]
like thisto define those functions over [a], so we can declare the following instance
in which the context Info a appears, making clear that we are only providing in-
formation about lists of objects for which we already have information about the
individual members.
We can complete the definition like this
Limitations
Instances in Haskell are global, so that once you have declared an instance for a type,
that instance is the one that you will have to use with that type: in particular it isn’t
possible to make instances local to a module or set of modules. If you still want to
do this, the mechanism to use would be to define a ‘wrapped’ type, like
either be a base type like Int, or consist of a type constructor like [...], (...,...)
or Shape applied to distinct type variables.
We will not be able, for example, to declare (Float,Float) as an instance; nor
can we use named types (introduced by a type definition). More details of the
mechanism can be found in the Haskell 2010 report (Marlow 2010).
The problem with doing this is that your program becomes reliant on GHC rather
than Haskell 2010, which is supported by a number of compilers, and also runs the
risk of depending on a feature which might change. Sufficiently many Haskell pro-
grammers are prepared to take little risks like this that it is difficult to find a project
of any size which is fully Haskell 2010 compliant.
Default definitions
To return to our example of equality, the Haskell equality class is in fact defined by
class Eq a where
(==), (/=) :: a -> a -> Bool
x /= y = not (x==y)
x == y = not (x/=y)
To the equality operation is added inequality, /=. As well as this, there are default
definitions of /= from == and of == from /=. These definitions have two purposes;
they give a definition over all equality types, but as defaults they can overridden by
an instance declaration.
At any instance a definition of at least one of == and /= needs to be supplied for
there to be a proper definition of (in)equality, but a definition of either is sufficient
to give both, by means of the defaults.
It is also possible to define both of the operations in an instance delaration , so
that if we wanted to define a different version of /= over Bool, we could add to our
instance declaration for Bool the line
In our Info example you will probably have noticed that for many of the base types
we simply said that the size of all the objects was one; we can add this as a default
to the definition of Info like this:
Default or top-level?
If we want to stop a default being overridden, we should remove the operation from
the class, and instead give its definition at the top level and not in the signature. In
the case of the operation /= in Eq we would give the top-level definition
x /= y = not (x == y)
Derived classes
Functions and instances can depend upon types being in classes; this is also true of
classes. The simplest example in Haskell is the class of ordered types, Ord. To be
ordered, a type must carry the operations >, >= and so on, as well as the equality
operations. We say
class Eq a => Ord a where
(<), (<=), (>), (>=) :: a -> a -> Bool
max, min :: a -> a -> a
compare :: a -> a -> Ordering
For a type a to be in the class Ord, we must supply over a definitions of the operations
of Eq as well as the ones in the signature of Ord. Given a definition of < we can supply
default definitions of the remaining operations of Ord. For instance,
x <= y = (x < y || x == y)
x > y = y < x
13.3. SIGNATURES AND INSTANCES 311
We will explain the type Ordering and the function compare in Section 13.4.
A simple example of a function defined over types in the class Ord is the insertion
sort function iSort of Chapter 7. Its most general type is
Indeed, any sorting function (which sorts using the ordering given by <=) would be
expected to have this type.
From a different point of view, we can see the class Ord as inheriting the opera-
tions of Eq; inheritance is one of the central ideas of object-oriented programming.
Multiple constraints
In the contexts we have seen so far, we have a single constraint on a type, such as Eq
a. There is no reason why we should not have multiple constraints on types. This
section introduces the notation we use, and some examples where it is needed.
Suppose we wish to sort a list and then show the results as a string. We can write
To sort the elements, we need the list to consist of elements from an ordered type,
as we saw above. To convert the results to a String we need [a] to be in the Show
class (which we discuss in detail in the next section). To do this it is sufficient for
each element of type a to be printable, and so the type of vSort is
showing that a must be in both the classes Ord and Show. Such types include Bool,
[Char] and so on.
In a similar way, suppose we are to use lookupFirst, and then make the results
visible. We write
We have twin constraints again on our list type [(a,b)]. We need to be able to
compare the first halves of the pairs, so Eq a is required. We also want to turn the
second halves into strings, so needing Show b. This gives the type
which shows that a pair of types in Eq again belongs to Eq. Multiple constraints can
also occur in the definition of a class,
In such a declaration, the class inherits the operations of both Ord and Show.
In this particular case, the class declaration contains an empty signature. To be
in OrdShow, the type a must simply be in the classes Ord and Show. We could then
modify the type of vSort to say
infoCheck2 property =
and (map (infoCheck.property) examples) (infoCheck.2)
Note that infoCheck2 uses infoCheck in its definition. Similarly we can define
infoCheck3 and so on. We could stop here, but we can do better, using overloading
to define a single infoCheck function, just as there is a single quickCheck function.
To do this we define another type class, and say that a type is Checkable if it’s
something that can be checked by applying it to the examples given in an Info type:
In short, we can apply infoCheck to any type where the argument types are in the
Info class, and where the result is a Bool, just as in the original definition of the
quickCheck function.
Summary
This section has explained the basic details of the class mechanism in Haskell. We
have seen that a class definition specifies a signature, and that in defining an in-
stance of a class we must provide definitions of each of the operations of the sig-
nature. These definitions override any default definitions which are given in the
class declaration. Contexts were seen to contain one or more constraints on the
type variables which appear in polymorphic types, instance declarations and class
declarations.
Exercises
13.4 How would you make Move, playing cards (as defined in Section 6.8), and triple
types, (a,b,c), into Info types?
13.5 [Harder] Moving beyond Haskell 2010 to use the -XFlexibleInstances for
GHCi, declare instances of Info for Int -> Bool and Int -> Int.
13.6 Give an instance of Info for the Float type, and using this re-define the in-
stance of Info for the Shape type.
where pairs and lists should be ordered lexicographically, like the words in a
dictionary.
314 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
Equality: Eq
Equality was described above; to recap, we define it by
class Eq a where
(==), (/=) :: a -> a -> Bool
x /= y = not (x==y)
x == y = not (x/=y)
Ordering: Ord
Similarly, we build the ordered class on Eq:
class (Eq a) => Ord a where
compare :: a -> a -> Ordering
(<), (<=), (>=), (>) :: a -> a -> Bool
max, min :: a -> a -> a
The data type Ordering contains three values LT, EQ and GT, which represent the
three possible outcomes from comparing two elements in the ordering, and is de-
fined thus:
data Ordering = LT | EQ | GT
The advantage of using compare is that a single function application decides the
exact relationship between two inputs, whereas when using the ordering operators
– which return Boolean results – two comparisons might well be necessary. Indeed,
we see this in the default definition of compare from == and <=, where two tests are
needed to reach the results LT and GT.
compare x y
| x == y = EQ
| x <= y = LT
| otherwise = GT
The defaults also contain definitions of the ordering operators from compare:
x <= y = compare x y /= GT
x < y = compare x y == LT
x >= y = compare x y /= LT
x > y = compare x y == GT
13.4. A TOUR OF THE BUILT-IN HASKELL CLASSES 315
Eq Show Read
All except All except All except
IO, (->) IO, (->) IO, (->)
Enum Real
Bool, Char, (), Int, Integer, Fractional
Float, Double, Double, Float, Double
Int, Integer Float
RealFloat
Float, Double
Monad Functor
IO, [], Maybe IO, [], Maybe
There are default definitions for all the operations of Ord, but we need to supply an
implementation of either compare or <= in order to give an instance of Ord.
Finally we have default definitions for the maximum and minimum operations,
max x y
| x <= y = y
| otherwise = x
min x y
| x <= y = x
| otherwise = y
316 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
Most Haskell types belong to these equality and ordering classes: among the ex-
ceptions are function types, and some of the abstract data types we meet below in
Chapter 16.
Enumeration: Enum
It is useful to generate lists like [2,4,6,8] using the enumeration expression
[2,4 .. 8]
but enumerations can be built over other types as well: characters, floating-point
numbers, and so on. The class definition is
class (Ord a) => Enum a where
succ, pred :: a -> a
toEnum :: Int -> a
fromEnum :: a -> Int
enumFrom :: a -> [a] -- [n .. ]
enumFromThen :: a -> a -> [a] -- [n,m .. ]
enumFromTo :: a -> a -> [a] -- [n .. m]
enumFromThenTo :: a -> a -> a -> [a] -- [n,n’ .. m]
where enumFromTo and enumFromThenTo have default definitions, which we leave
as exercises for the reader.
The signature of the class also contains operations fromEnum and toEnum which
convert between the type and Int. Finally, the class contains succ and pred which
step through the enumeration upwards and downwards: when succ is called at the
greatest element, an error is returned.
Confusingly, the Haskell report states that ‘these functions [toEnum and fromEnum]
are not meaningful for all instances of Enum’, and using these operations over floating-
point values or full precision integers will result in a run-time error.
Full instances of the class include Int, Char, Bool and other finite types like
Ordering.
• The fixed precision integers, Int, and the full precision integers, Integer,
which represent all integers faithfully.
• The floating-point numbers, Float, and the double-precision floating-point
numbers, Double.
• Rational numbers, that is fractions, represented as ratios of integers; built-in
is the type Rational of Integer fractions.
• Complex numbers, which can be built over other types such as Float.
The design also required that the usual operations like + and / and literals such as 23
and 57.4 would be overloaded. For instance, Int and Integer will carry identical
operations2 and have identical literals, as indeed will Float and Double; a guide
to the operations over integers and floats was given in Sections 3.2 and 3.6. This
overloading can lead to situations where the type of an expression is undetermined;
in such a case we can give an explicit type to an expression, thus:
(2+3)::Int
The Haskell report (Marlow 2010), Section 4.3.4, discusses a mechanism by which
a default type can be given to numeric expressions. These default directives mean
that whole numbers are taken to be Integer and others to be Double in the absence
of any other type information. This can be seen in action in this snapshot of GHCi:
x - y = x + negate y
2 Apart from (de)coding of Char, take, drop and so forth.
13.4. A TOUR OF THE BUILT-IN HASKELL CLASSES 319
This signature has the effect that all numeric types carry equality and show func-
tions, together with addition, subtraction, multiplication and related operations. It
is also possible to convert an Int or and Integer into a value of any numeric type.
Integer literals are of any numeric type, so that, for example
2 :: Num a => a
The integer types belong to the class Integral among whose signature functions
are
quot, rem :: a -> a -> a
div, mod :: a -> a -> a
which give two variants of integer division, ‘quot‘ truncating towards zero, and
‘div‘ truncating below.
Numbers with fractional parts have a substantially richer class structure. Literals
of this kind belong to every type in the Fractional class,
2.3 :: Fractional a => a
which extends Num with fractional division and reciprocal,
class (Num a) => Fractional a where
(/) :: a -> a -> a
recip :: a -> a
fromRational :: Rational -> a
recip x = 1 / x
The floating-point numbers in Float and Double belong to the class Floating,
which carries the ‘mathematical’ functions. A part of its signature follows,
class (Fractional a) => Floating a where
pi :: a
exp, log, sqrt :: a -> a
(**), logBase :: a -> a -> a
sin, cos, tan :: a -> a
....
and the full signature is to be found in Prelude.hs. Further details of this and the
complex and rational types can be found in the prelude, libraries and the Haskell
documentation.
Derived instances
When a new data type is introduced, it comes with facilities for pattern matching
but no other pre-defined functions. On the other hand, it’s possible to come up
with standard definitions of equality, ordering, show and read functions for these
types, and this deriving mechanism is the bit of ‘Haskell magic’ which we mentioned
in Sections 4.3 and 5.3 when we introduced data type definitions. If we make a
definition like
320 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
then definitions of == and show which “do the obvious thing” are synthesised for
this type. This could be done for all the standard classes, but we could also choose
to define instances of other standard classes for ourselves, so that we might read
name, age pairs from a comma separated variable (CSV) file, rather than using the
standard definition of read, which would expect input of the form
Exercises
13.10 Investigate the Haskell definition of ‘<’ on the types Bool and (t1 ,t2 ,...,tk ).
13.12 Using your answer to the previous question, or otherwise, describe how you
would make Bool -> Bool an instance of the class Show. (Note, however,
that this will not be legitimate Haskell 2010, since Bool -> Bool is not of the
right form for an instance declaration; you can achieve this using the GHC
option -XFlexibleInstances.)
13.13 How can you write a general instance for Show for function types: you could
do this by showing a “sample” of the values from the function, that is showing
how a sample of inputs are sent to the corresponding outputs.
13.14 Some types are not enumerated in the sense that they can be listed from small-
est to largest: a good example is the Move type from the Rock - Paper - Scissors
game. Define a type class to which the Move type can belong, and give an in-
stance for Move. What other types can you think of giving an instance for: give
some examples and their instances, too.
and define instances of Show and Num for this type. The show function should
display numbers in the form of Roman numerals, so that
13.16 [Harder] For the data type Roman define an instances of the Read class, which
is the inverse of the show function in the previous question.
322 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
[True,’N’,False] :: [ShowType]
Moreover, to convert such a list to a String we could write
so that it can be applied to elements of [Bool], [Char] and so on, but not to hetero-
geneous lists like [True,’N’,False] which are not legitimately typed in Haskell.
Java allows users to define interfaces, which consist of a signature. A part of a class
definition can say which interfaces the class implements. This is very like the way
in which Haskell types are made instances of type classes, except that in Haskell it
is not necessary to make the instance declaration a part of the type definition itself.
This has the effect of allowing post hoc extensions to the operations supported by a
type, in a way which is not possible for a class in Java.
13.5. TYPE CHECKING AND TYPE INFERENCE: AN OVERVIEW 323
’w’ :: Char
flip :: (a -> b -> c) -> (b -> a -> c)
elem :: Eq a => a -> [a] -> Bool
Strong typing means that we can check whether or not expressions we wish to eval-
uate or definitions we wish to use obey the typing rules of the language without any
evaluation taking place. The benefit of this is obvious: we can catch a whole lot of
errors before we run a program.
prodFun f g = \x -> (f x, g x)
either in a module or directly in GHCi, and then ask for its type in CHGi like this:
Because of this facility, some Haskellers never write a type declaration, but others,
including the author, always do: why?
• The type of an object is the most important single piece of documentation for
the object, since it tells us how it can be used – what arguments need to be
passed to it, and what type the result has – without us having to understand
precisely how it is implemented.
• We can use a type declaration to give a more specific type to a definition. This
was the mechanism underlying the first part of the book, which turned poly-
morphic functions into monomorphic versions. To be clear, if we define prodFun
like this
prodFun :: (Int -> Bool) -> (Int -> Char) -> Int -> (Bool,Char)
prodFun f g = \x -> (f x, g x)
then it will have this more specific type. It is not difficult to recover the most
general type for the definition: just comment out the type declaration.
• In writing a type declaration we are saying what type we think a function has.
We may have not got this right, and the function is properly typed, but has a
different type. For instance, typing
324 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
fun True 0 = 0
fun True n = n-1
fun _ n = n
In a case like this it is useful to know that we were wrong, and then we can
either correct the type declaration, or modify the function so that it has the
type we wanted. Here the problem is fixed by swapping the types of the two
arguments.
There is one case where we do need to use type declarations or annotations: this
is in resolving ambiguity due to overloading (we talked briefly about this earlier, in
Section 13.4, page 318).
A Hoogle search of the standard prelude and libraries reveals just one function of
this type, namely nub, which does exactly what we want. Plainly in practice there
might be multiple matches (or missed matches because of the choice of parameter
order) but nonetheless the types provide a valuable way into the Haskell library.
Overview
In the remainder this chapter we give an informal overview of the way in which types
are checked. We start by looking at how type checking works in a monomorphic
framework, in which every properly typed expression has a single type. Building on
this, we then look at the polymorphic case, and see that it can be understood by look-
ing at the constraints put on the type of an expression by the way that the expression
is constructed. Crucial to this is the notion of unification, through which constraints
are combined. We conclude the chapter by looking at the contexts which contain
information about the class membership of type variables, and which thus manage
overloading.
13.6. MONOMORPHIC TYPE CHECKING 325
Expressions
In general, an expression is either a literal, a variable or a constant or it is built up by
applying a function to some arguments, which are themselves expressions.
The case of function applications includes rather more than we might at first
expect. For example, we can see list expressions like [True,False] as the result of
applying the constructor function, ‘:’, thus: True:[False]. Also, operators and the
if . . . then . . . else construct act in exactly the same way as functions, albeit with a
different syntax.
The rule for type checking a function application is set out in the following dia-
gram, where we see that a function of type s -> t must be applied to an argument
of type s. A properly typed application results in an expression of type t.
e must have
type s
(f e)
We now look at two examples. First we take (not False) && True, a correctly
typed expression of type Bool,
Bool Bool
Bool
326 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
The application of not to False results in an expression of type Bool. The second
argument to && is also a Bool, so the application of && is correctly typed, and gives a
result of type Bool.
If we modify the example to (not ’c’) && True, we now see a type error, since
a character argument, ’c’, is presented to an operator expecting a Bool argument,
not.
Char Bool
Bool expected
but Char
inferred
The GHCi error message for this indicates the cause of the problem:
Couldn’t match expected type ‘Bool’ against inferred type ‘Char’
In the first argument of ‘not’, namely ’c’
In the first argument of ‘(&&)’, namely ‘(not ’c’)’
In the expression: (not ’c’) && True
Function definitions
In type-checking a monomorphic function definition such as
f :: t1 -> t2 -> ... -> tk -> t (fdef)
f p1 p2 ... pk
| g1 = e1
| g2 = e2
...
| gl = el
we need to check three things.
• Each of the guards gi must be of type Bool.
• The pattern pj must be consistent with type of that argument, namely tj.
A pattern is consistent with a type if it will match (some) elements of the type. We
now look at the various cases. A variable is consistent with any type; a literal is con-
sistent with its type. A pattern (p:q) is consistent with the type [t] if p is consistent
13.7. POLYMORPHIC TYPE CHECKING 327
with t and q is consistent with [t]. For example, (0:xs) is consistent with the type
[Int], and (x:xs) is consistent with any type of lists. The other cases of the defini-
tion are similar.
This concludes our discussion of type checking in the monomorphic case; we
turn to polymorphism next.
Exercises
13.17 Predict the type errors you would obtain by defining the following functions
f n = 37+n
f True = 34
g 0 = 37
g n = True
h x
| x>0 = True
| otherwise = 37
k x = 34
k 0 = 35
Check your answers by typing each definition into a Haskell script, and load-
ing the script into GHCi. Remember that you can use :type to give the type of
an expression.
Polymorphism
We are familiar with functions like
in fact containing all the types [t] -> Int where t is a monotype, that is a type not
containing type variables.
When we apply length we need to determine at which of these types length is
being used. For example, when we write
length [’c’,’d’]
we can see that length is being applied to a list of Char, and so we are using length
at type [Char] -> Int.
Constraints
How can we explain what is going on here in general? We can see different parts
of an expression as putting different constraints on its type. Under this interpre-
tation, type checking becomes a matter of working out whether we can find types
which meet the constraints. We have seen some informal examples of this when we
discussed the types of map and filter in Section 10.2. We consider some further
examples now.
Example 1
The argument of f is a pair, and we consider separately what constraints there are on
the types of x and y. x is completely unconstrained, as it is returned as the first half
of a pair. On the other hand, y is used within the expression [’a’ .. y], which de-
notes a range within an enumerated type, starting at the character ’a’. This forces
y to have the type Char, and gives the type for f:
Example 2
g (m,zs) = m + length zs
What constraints are placed on the types of m and zs in this definition? We can see
that m is added to something, so m must have a numeric type – which one it is remains
to be seen. The other argument of the addition is length zs, which tells us two
things.
13.7. POLYMORPHIC TYPE CHECKING 329
g (m,zs) = m + length zs
First, we see that zs will have to be of type [b], and also that the result is an Int.
This forces + to be used at Int, and so forces m to have type Int, giving the result
g :: (Int,[b]) -> Int
Example 3
h = g . f
input type of g is
output type of f
Here we should recall the meaning of types which involve type variables; we can see
them as shorthand for sets of types. The output of f is described by (a,[Char]),
and the input of g by (Int,[b]). We therefore have to look for types which meet
both these descriptions. We will now look at this general topic, returning to the ex-
ample in the course of this dicussion.
Unification
How are we to describe the types which meet the two descriptions (a,[Char]) and
(Int,[b])?
(Bool,[Char]) (Int,[Int])
(Int,[Char])
(a->a,[Char]) (Int,[[c]])
(a,[Char]) (Int,[b])
As sets of types, we look for the intersection of the sets given by (a,[Char]) and
(Int,[b]). How can we work out a description of this intersection? Before we do
this, we revise and introduce some terminology.
330 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
Example 3 (continued)
(a,[Char]) (Int,[b])
(Int,[Char])
with a single type resulting. This means that the type a has to be Int and so the type
of the function h = g.f is
Unification, revisited
Unification need not result in a monotype. In the example of unifying the types
(a,[a]) and ([b],c),
(a,[a]) ([b],c)
(b,[b])
the result is the type ([b],[[b]]). This is because the expression (a,[a]) con-
strains the type to have in its second component a list of elements of the first com-
ponent type, while the expression ([b],c) constrains its first component to be a
list. Thus satisfying the two gives the type ([b],[[b]]).
In the last example, note that there are many common instances of the two type
expressions, including ([Bool],[[Bool]]) and ([[c]],[[[c]]]), but neither of
these examples is the unifier, since ([b],[[b]]) is not an instance of either of
them. On the other hand, they are each instances of ([b],[[b]]), as it is the most
general common instance, and so the unifier of the two type expressions.
Not every pair of types can be unified: consider the case of [Int] -> [Int]
and a -> [a].
13.7. POLYMORPHIC TYPE CHECKING 331
Unifying the argument types requires a to become [Int], while unifying the result
types requires a to become Int; clearly these constraints are inconsistent, and so
the unification fails.
Type-checking expressions
As we saw in Section 13.6, function application is central to expression formation.
This means that type checking also hinges on function applications.
(f e)
unify s and u
Example 4
As an example, consider the application map Circle where Circle is one of the
constructor functions for the Shape type.
Unifying a -> b and Float -> Shape results in a becoming Float and b becom-
ing Shape; this gives
332 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
foldr f s [] = s (foldr.1)
foldr f s (x:xs) = f x (foldr f s xs) (foldr.2)
which could be used to fold an operator into a list, as in
foldr :: (... -> ... -> ...) -> b -> [a] -> ...
Then we can picture the definition thus:
s has
type b x has
type a
foldr f s [] = s
foldr f s (x:xs) = f x (foldr f s xs)
s is the result of the first equation, and so the result type of the foldr function itself
will be b, the type of s
13.7. POLYMORPHIC TYPE CHECKING 333
foldr :: (... -> ... -> ...) -> b -> [a] -> b
Finally, the result of the second equation is an application of f; this result must have
the same result type as the foldr itself, b.
With this insight about the type of foldr we were able to see that foldr could be
used to define another whole cohort of list functions, such as an insertion sort,
in which ins has the type Ord a => a -> [a] -> [a].
The variable xs is forced to have type [Bool] and type [Integer]; it is forced to
be polymorphic, in other words. This is not allowed in Haskell, as there is no way of
expressing the type of funny. It might be thought that
was a correct type, but this would mean that funny would have all the instance types
which it clearly does not. We conclude that constants and variables are treated dif-
ferently: constants may very well appear at different incompatible types in the same
expression, variables cannot.
What is the significance of disallowing the definition (funny) but allowing the
definition (expr)? Taking (expr) first, we have a polymorphic definition of the
form [] :: [a] and an expression in which [] occurs twice; the first occurrence
is at [Bool], the second at [Integer]. To allow these independent uses to occur,
we type-check each use of a polymorphic definition with different type variables, so
that a constraint on one use does not affect any of the others.
On the other hand, how is the definition of (funny) disallowed? When we type
check the use of a variable we will not treat each instance as being of an indepen-
dent type. Suppose we begin with no constraint on xs, so xs::t, say. The first oc-
currence of xs forces xs::[Bool], the second requires xs::[Integer]; these two
constraints cannot be satisfied simultaneously, and thus the definition (funny) fails
to type check.
The crucial point to remember from this example is that the definition of a func-
tion can’t force any of its arguments to be polymorphic.
Function definitions
In type checking a function definition like (fdef) on page 326 above we have to
obey rules similar to the monomorphic case.
• The value ei returned in each clause must have a type si which is at least as
general as t; that is, si must have t as an instance.
• The pattern pj must be consistent with type of that argument, namely tj.
We take up a final aspect of type checking – the impact of type classes – in the next
section.
Exercises
13.18 Do the following pairs of types – listed vertically – unify? If so, give a most
general unifier for them; if not, explain why they fail to unify.
13.19 Show that we can unify (a,[a]) with (b,c) to give (Bool,[Bool]).
f :: (a,[a]) -> b
f :: (a,[a]) -> a
h x = f x x ?
13.23 How can you use the Haskell system to check whether two type expressions
are unifiable, and if so what is their unification? Hint: you can make dummy
definitions in Haskell in which the defined value, zircon say, is equated with
itself:
zircon = zircon
Values defined like this can be declared to have any type you wish.
13.24 [Harder] Recalling the definitions of curry and uncurry from Section 11.4,
what are the types of
curry id
uncurry id
curry (curry id)
uncurry (uncurry id)
uncurry curry
curry uncurry
curry curry
13.25 [Harder] Give an algorithm which decides whether two type expressions are
unifiable. If they are, your algorithm should return a most general unifying
substitution; if not, it should give some explanation of why the unification
fails.
336 CHAPTER 13. OVERLOADING, TYPE CLASSES AND TYPE CHECKING
member [] y = False
member (x:xs) y = (x==y) || member xs y
because x and y of type a are compared for equality in the definition, thus forcing
the type a to belong to the equality class Eq.
This section explores the way in which type checking takes place when overload-
ing is involved; the material is presented informally, by means of an example.
Suppose we are to apply the function member to an expression e, whose type is
Informally, e is a list of lists of objects, which belong to a type which carries an or-
dering. In the absence of the contexts we would unify the type expressions, giving
and so giving the application member e the type [b] -> Bool. We do the same
here, but we also apply the unification to the contexts, producing the context
so that any instance of Ord is automatically an instance of Eq; this means that
we can simplify (ctx.2) to
Ord b
Exercises
13.26 Give the type of each of the individual conditional equations which follow, and
discuss the type of the function which together they define.
13.27 Define a polymorphic sorting function, and show how its type is derived from
the type of the ordering relation
13.28 Investigate the types of the following numerical functions; you will find that
the types refer to some of the built-in numeric classes.
mult x y = x*y
divide x = x ‘div‘ 2
share x = x / 2.0
Summary
This chapter has shown how names such as read and show and operators like + can
be overloaded to have different definitions at different types. The mechanism which
enables this is the system of Haskell classes. A class definition contains a signature
which contains the names and types of operations which must be supplied if a type
is to be a member of the class. For a particular type, the function definitions are
contained in an instance declaration.
In giving the type of a function, or introducing a class or an instance, we can
supply a context, which constrains the type variables occurring. Examples include
member :: Eq a => [a] -> a -> Bool
instance Eq a => Eq [a] where ....
class Eq a => Ord a where ....
In the examples, it can be seen that member can only be used over types in the class
Eq. Lists of a can be given an equality, provided that a itself can; types in the class Ord
must already be in the class Eq. After giving examples of the various mechanisms,
we looked at the classes in the standard preludes of Haskell.
We concluded the chapter with a discussion of how type checking of expressions
and definitions is performed in Haskell, initially in the monomorphic case, and then
in full generality with polymorphic and overloaded functions. In that case we saw
type checking as a process of extracting and consolidating constraints which come
from the unification of type expressions which contain type variables.
Chapter 14
Algebraic types
So far in our discussion of Haskell we have been able to model entities using
• the base types, Int, Float, Bool and Char, and
• composite types: tuple types, (t1 ,t2 ,...,tn ); list types, [t1 ]; and function
types, (t1 -> t2 ); where t1 , . . . , tn are themselves types,
• algebraic types, including enumerated types, as introduced in Section 4.3, and
product and sum type, first given in Section 5.3.
This gives a wide range of types to use in modelling different domains in Haskell.
This chapter completes our coverage of the topic of algebraic types and looks at two
extensions in some detail:
• The types can be recursive; we can use the type we are defining, Typename,
as (part of) any of the component types, as in the definition of numeric trees
“NTrees”:
339
340 CHAPTER 14. ALGEBRAIC TYPES
12
10 17
14 20
Exercises
14.1 Reimplement the library database of Section 5.7 to use an algebraic type like
People rather than a pair. Compare the two approaches to this example.
14.2 The library database of Section 5.7 is to be extended in the following ways.
Explain how you would modify the types used to implement the database, and
how the function types might be changed. The system should perform the fol-
lowing operations. For each case, give the types and definitions of the func-
tions involved.
342 CHAPTER 14. ALGEBRAIC TYPES
17
10 14 20
What other functions would have to be defined to make the system usable?
Give their types, but not their definitions.
Finally, we have already used the type of lists: a list is either empty ( []) or is built
from a head and a tail – another list – using the list constructor ‘:’. Lists will provide
a good guide to using recursive (and polymorphic) definitions. In particular they
suggest how ‘general’ polymorphic higher-order functions over other algebraic types
are defined, and how programs are verified. We now look at some examples in more
detail.
Expressions
The type Expr gives a model of the simple numerical expressions discussed above.
These might be used in implementing a simple numerical calculator, for instance.
• evaluate it;
• turn it into a string, which can then be printed;
• estimate its size – count the operators, say.
Each of these functions will be defined in the same way, using primitive recursion.
As the type is itself recursive, it is not a surprise that the functions which handle
the type are also recursive. Also, the form of the recursive definitions follows the
recursion in the type definition. For instance, to evaluate an operator expression we
work out the values of the arguments and combine the results using the operator.
eval (Lit n) = n
eval (Add e1 e2) = (eval e1) + (eval e2)
eval (Sub e1 e2) = (eval e1) - (eval e2)
• At the non-recursive, base cases – (Lit n) here – the value is given outright.
• At the recursive cases, the values of the function at the sub-expressions from
which the expression is formed – eval e1 and eval e2 here – can be used in
calculating the result.
Trees of integers
Trees of integers like that in Figure 14.1 can be modelled by the type
data NTree = NilT |
Node Integer NTree NTree
The null tree is given by NilT, and the trees in Figure 14.2 by
Node 10 NilT NilT
Node 17 (Node 14 NilT NilT) (Node 20 NilT NilT)
Definitions of many functions are primitive recursive. For instance,
sumTree,depth :: NTree -> Integer
sumTree NilT = 0
sumTree (Node n t1 t2) = n + sumTree t1 + sumTree t2
depth NilT = 0
depth (Node n t1 t2) = 1 + max (depth t1) (depth t2)
with, for example,
sumTree (Node 3 (Node 4 NilT NilT) NilT) = 7
depth (Node 3 (Node 4 NilT NilT) NilT) = 2
As another example, take the problem of finding out how many times a number, p
say, occurs in a tree. The primitive recursion suggests two cases, depending upon
the tree.
• For a null tree, NilT, the answer must be zero.
• For a non-null tree, (Node n t1 t2), we can find out how many times p oc-
curs in the sub-trees t1 and t2 by two recursive calls; we have to make a case
split depending on whether p occurs at the particular node, that is depending
on whether or not p==n.
14.2. RECURSIVE ALGEBRAIC TYPES 345
occurs NilT p = 0
occurs (Node n t1 t2) p
| n==p = 1 + occurs t1 p + occurs t2 p
| otherwise = occurs t1 p + occurs t2 p
The exercises at the end of the section give a number of other examples of func-
tions defined over trees using primitive recursion. We next look at a particular ex-
ample where a different form of recursion is used.
Rearranging expressions
The next example shows a definition which uses a more general recursion than we
have seen so far. After showing why the generality is necessary, we argue that the
function we have defined is total: it will give a result on all well-defined expressions.
The operation of addition over the integers is associative, so that the way in
which an expression is bracketed is irrelevant to its value. We can, therefore, de-
cide to bracket expressions involving ‘+’ in any way we choose. The aim here is to
write a program to turn expressions into right bracketed form, as shown in Figure
14.3 and in the following table:
(2+3)+4 2+(3+4)
((2+3)+4)+5 2+(3+(4+5))
((2-((6+7)+8))+4)+5 (2-(6+(7+8)))+(4+5)
which is primitive recursive: on the right-hand side of their definition the function
try is only used on sub-expressions of the argument. This function will have the
effect of transforming (AddL) to (AddR), but unfortunately (AddExL) will be sent
to (AddExR):
((2+3)+4)+5 (AddExL)
(2+3)+(4+5) (AddExR)
346 CHAPTER 14. ALGEBRAIC TYPES
+ +
+ +
The other cases in the definition make sure that the parts of an expression are rear-
ranged as they should be.
The equation (Add.2) will only be applied to the cases where (Add.1) does not
apply – this is when e1 is either a Sub or a Lit expression. This is always the case in
pattern matching; the first applicable equation is used.
When we use primitive recursion we can be sure that the recursion will termi-
nate to give an answer: the recursive calls are only made on smaller expressions and
so, after a finite number of calls to the function, a base case will be reached.
The assoc function is more complicated, and we need a more subtle argument
to see that the function will always give a result. The equation (Add.1) is the tricky
one, but intuitively, we can see that some progress has been made – some of the
‘weight’ of the tree has moved from left to right. In particular, one addition symbol
has swapped sides. None of the other equations moves a plus in the other direction,
so that after applying (Add.1) a finite number of times, there will be no more ex-
14.2. RECURSIVE ALGEBRAIC TYPES 347
posed addition symbols at the top level of the left-hand side. This means that the
recursion cannot go on indefinitely, and so the function always leads to a result.
In Section 14.7 we’ll look at specifying QuickCheck properties for functions over
algebraic types, including assoc, as well as showing how some of these can be proved
by induction.
Mutual recursion
In describing one type, it is often useful to use others; these in turn may refer back
to the original type: this gives a pair of mutually recursive types. A description of a
person might include biographical details, which in turn might refer to other people.
For instance:
Suppose that we want to define a function which shows information about a per-
son as a string. Showing this information will require us to show some biographical
information, which itself contains further information about people. We thus have
two mutually recursive functions:
Exercises
14.5 Add the operations of multiplication and integer division to the type Expr, and
redefine the functions eval, show and size to include these new cases. What
does your definition of eval do when asked to perform a division by zero?
14.6 Instead of adding extra constructors to the Expr type, as in the previous ques-
tion, it is possible to factor the definition thus:
Show how the functions eval, show and size are defined for this type, and
discuss the changes you have to make to your definitions if you add the extra
operation Mod for remainder on integer division.
14.8 Complete the redefinition of functions over Expr after it has been defined us-
ing the infix constructors :+: and :-:.
14.9 Define functions to return the left- and right-hand sub-trees of an NTree.
14.11 Define functions to find the maximum and minimum values held in an NTree.
14.12 A tree is reflected by swapping left and right sub-trees, recursively. Define
a function to reflect an NTree. What is the result of reflecting twice,
reflect . reflect?
which turn a tree into a list. The function collapse should enumerate the left
sub-tree, then the value at the node and finally the right sub-tree; sort should
sort the elements in ascending order. For instance,
14.14 Complete the definitions of showPerson and showBio which were left incom-
plete in the text.
14.15 It is possible to extend the type Expr so that it contains conditional expres-
sions, If b e1 e2, where e1 and e2 are expressions, and b is a Boolean ex-
pression, a member of the type BExp,
The expression
If b e1 e2
has the value of e1 if b has the value True and otherwise it has the value of e2.
by mutual recursion, and extend the function show to show the redefined type
of expressions.
data Pairs a = Pr a a
and example elements of the type are
Pr 2 3 :: Pairs Integer
Pr [] [3] :: Pairs [Int]
Pr [] [] :: Pairs [a]
A function to test the equality of the two halves of a pair is given by
Lists
The built-in type of lists can be given by a definition like
infixr 5 :::
where the syntax [a], [] and ‘:’ is used for List a, NilList and :::. Note that we
have given a fixity declaration for ::: to give it the same fixity and associativity as
:, we can therefore write expressions like this:
*Chapter14> 2+3 ::: 4+5 ::: NilL
5 ::: (9 ::: NilL)
much as lists are written with :.
Lists form a useful paradigm for recursive polymorphic types. In particular, we
can see the possibility of defining useful families of functions over such types, and
the way in which program verification can proceed by induction over the structure
of a type.
Binary trees
The trees of Section 14.2 carry numbers at each node; there is nothing special about
numbers, and we can equally well say that they have elements of an arbitrary type
at the nodes:
data Tree a = Nil | Node a (Tree a) (Tree a)
deriving (Eq,Ord,Show,Read)
The definitions of depth and occurs carry over unchanged:
depth :: Tree a -> Integer
depth Nil = 0
depth (Node n t1 t2) = 1 + max (depth t1) (depth t2)
as do many of the functions defined in the exercises at the end of Section 14.2. One of
these is the function collapsing a tree into a list. This is done by visiting the elements
of the tree ‘inorder’, that is visiting first the left sub-tree, then the node itself, then
the right sub-tree, thus:
collapse :: Tree a -> [a]
collapse Nil = []
collapse (Node x t1 t2)
= collapse t1 ++ [x] ++ collapse t2
For example,
collapse (Node 12
(Node 34 Nil Nil)
(Node 3 (Node 17 Nil Nil) Nil))
= [34,12,17,3]
Various higher-order functions are definable, also,
mapTree :: (a -> b) -> Tree a -> Tree b
mapTree f Nil = Nil
mapTree f (Node x t1 t2)
= Node (f x) (mapTree f t1) (mapTree f t2)
We shall return to trees in Section 16.7, where particular ‘search’ trees form a case
study.
352 CHAPTER 14. ALGEBRAIC TYPES
Either a b
f
a
c
g
b
Members of the ‘union’ or ‘sum’ type are (Left x), with x::a, and (Right y) with
y::b. The ‘name or number’ type is given by Either String Int and
To define a function from Either a b to Int, say, we have to deal with two cases,
In the first case, the right-hand side takes x to an Int, so is given by a function from
a to Int; in the second case y is taken to an Int, thus being given by a function from
b to Int.
Guided by this, we can give a higher-order function which joins together two
functions defined on a and b to a function on Either a b. The definition follows,
and is illustrated in Figure 14.3.
14.3. POLYMORPHIC ALGEBRAIC TYPES 353
either f g (Left x) = f x
either f g (Right y) = g y
but in the next section we shall explore other ways of handling errors in more detail.
Exercises
14.16 Investigate which of the functions over trees discussed in the exercises of Sec-
tion 14.2 can be made polymorphic.
14.18 How would you define applyLeft using the function either?
14.19 Show that any function of type a -> b can be transformed into functions of
type
a -> Either b c
a -> Either c b
14.20 How could you generalize either to join so that it has type
You might find the answer to the previous exercise useful here, if you want to
define join using either.
The trees defined in the text are binary: each non-nil tree has exactly two sub-trees.
We can instead define general trees with an arbitrary list of sub-trees, thus:
Exercises
In each case give the type of the function that you have defined.
• attempts to divide by zero, to take the square root of a negative number, and
other arithmetical transgressions;
• attempts to take the head of an empty list – this is a special case of a definition
over an algebraic type from which one case (here the empty list) is absent.
This section examines the problem, giving three approaches of increasing sophisti-
cation. The simplest method is to stop computation and to report the source of the
problem. This is indeed what the Haskell system does in the cases listed above, and
we can do this in functions we define ourselves using the error function,
Dummy values
The function tail is supposed to give the tail of a list, and it gives an error message
on an empty list:
14.4. MODELLING PROGRAM ERRORS 355
Now, an attempt to take the tail of any list will succeed. In a similar way we could
say
so that division by zero gives some answer. For tl and divide there have been
obvious choices about what the value in the ‘error’ case should be; for head there is
not, and instead we can supply an extra parameter to head, which is to be used in
the case of the list being empty.
This approach is completely general; if a function f (of one argument, say) usually
raises an error when cond is True, we can define a new function
fErr y x
| cond = y
| otherwise = f x
This approach works well in many cases; the only drawback is that we have no way
of telling when an error has occurred, since we may get the result y from either the
error or the ‘normal’ case. Alternatively we can use an error type to trap and process
errors; this we look at now.
Error types
The previous approach works by returning a dummy value when an error has oc-
curred. Why not instead return an error value as a result? We define the type
which is effectively the type a with an extra value Nothing added. We can now define
a division function errDiv thus
356 CHAPTER 14. ALGEBRAIC TYPES
fErr x
| cond = Nothing
| otherwise = Just (f x)
The results of these functions are now not of the original output type, a say, but of
type Maybe a. These Maybe types allow us to raise an error, potentially. We can do
two things with a potential error which has been raised
These two operations are illustrated in Figure 14.4, and we define them now.
The function mapMaybe transmits an error value through the application of the
function g. Suppose that g is a function of type a -> b, and that we are to lift it to
operate on the type Maybe a. In the case of an argument Just x, g can be applied
to the x to give a result, g x, of type b; this is put into Maybe b by applying the
constructor function Just. On the other hand, if Nothing is the argument then
Nothing is the result.
mapMaybe :: (a -> b) -> Maybe a -> Maybe b
In trapping an error, we aim to return a result of type b, from an input of type Maybe
a; we have two cases to deal with
• in the Just case, we apply a function from a to b;
• in the Nothing case, we have to give the value of type b which is to be returned.
(This is rather like the value we supplied to hd earlier.)
The higher-order function which achieves this is maybe, whose arguments n and f
are used in the Nothing and Just cases respectively.
maybe n f Nothing = n
maybe n f (Just x) = f x
We can see the functions mapMaybe and maybe in action in the examples which fol-
low. In the first, a division by zero leads to a Nothing which passes through the
lifting to be trapped – 56 is therefore returned:
14.4. MODELLING PROGRAM ERRORS 357
g f
a b a
mapMaybe g maybe n f
Exercises
14.23 Using the functions mapMaybe and maybe, or otherwise, define a function
so that process xs n m takes the nth and mth items of the list of numbers xs,
and returns their sum. Your function should return 0 if either of the numbers
is not one of the indices of the list: for a list of length p, the indices are 0, . . . ,
p-1 inclusive.
14.24 Discuss the advantages and disadvantages of the three approaches to error
handling presented in this section.
358 CHAPTER 14. ALGEBRAIC TYPES
14.25 What are the values of type Maybe (Maybe a)? Define a function
which will ‘squash’ Just (Just x) to Just x and all other values to Nothing.
which composes two error-raising functions. How could you use mapMaybe,
the function composition operator and the squash function to define composeMaybe?
14.27 The Maybe type could be generalized to allow messages to be carried in the
Nothing part, thus:
We suppose that there are five basic editing operations on a string. We can
change one character into another, copy a character without modifying it, delete
or insert a character and delete (kill) to the end of the string. We also assume that
each operation has the same cost, except a copy which is free.
To turn the string "fish" into "chips", we could
kill the whole string, then insert the characters one-
f i s h by-one, at a total cost of six. An optimal solution will
copy as much of the string as possible, and is given by
The ‘...’ show that we have not yet said anything about the types of the con-
structors.
• Finally, for each of the constructors, we need to decide what its components
or arguments are. Some of the constructors – Copy, Delete and Kill – require
no information; the others need to indicate the new character to be inserted,
so
We now illustrate how other type definitions work in a similar way, before returning
to give a solution to the ‘edit distance’ problem.
Simulation
Suppose we want to model, or simulate, how the queues in a bank or Post Office
behave; perhaps we want to decide how many bank clerks need to be working at
particular times of the day. Our system will take as input the arrivals of customers,
and give as output their departures. Each of these can be modelled using a type.
• Inmess is the type of input messages. At a given time, there are two possibili-
ties:
Hence we have
• Similarly, we have Outmess, the type of output messages. Either no-one leaves
(None), or a person is discharged (Discharge). The relevant information they
carry is the time they have waited, together with when they arrived and their
service time. We therefore define
14.5. DESIGN WITH ALGEBRAIC DATA TYPES 361
The problem is to find the lowest-cost sequence of edits to take us from one string
to another. We can begin the definition thus:
transform [] [] = []
To transform the non-empty string st to [], we simply have to Kill it, while to
transform [] to st we have to Insert each of the characters in turn:
transform xs [] = [Kill]
transform [] ys = map Insert ys
In the general case, we have a choice: should we first use Copy, Delete, Insert or
Change? If the first characters of the strings are equal we should copy; but if not,
there is no obvious choice. We therefore try all possibilities and choose the best of
them:
How do we choose the best sequence? We choose the one with the lowest cost.
The cost is given by charging one for every operation except copy, which is equiva-
lent to ‘leave unchanged’.
prop_transformLength xs ys =
length (xs++ys) <= 15 ==>
cost (transform xs ys) <= length ys + 1
where we have guarded the test on the overall length of the two lists, for efficiency
reasons.
Secondly, the sequence of edits given by transform xs ys should indeed take the
string xs to ys when it is applied, so
prop_transform xs ys =
length (xs++ys) <= 15 ==>
edit (transform xs ys) xs == ys
We leave it as an exercise for the reader to define the function
Exercises
14.28 How would you modify the edit distance program to accommodate a Swap
operation, which can be used to transform "abxyz" to "baxyz" in a single
step?
14.29 Write a definition of the edit function described above, which when given a
list of edits and a string st, returns the sequence of strings given by applying
the edits to st in sequence.
14.30 Can you give other QuickCheck properties that you would expect the edit dis-
tance program to have? You could think, for example, about particular sorts of
inputs, e.g. where the input is an initial segment of the output.
14.31 Give a calculation of transform "cat" "am". What do you conclude about
the efficiency of the transform function?
14.6. ALGEBRAIC TYPES AND TYPE CLASSES 363
14.32 [Harder] Can you give a more efficient implementation of the function calcu-
lating the transform?
The remaining questions are designed to make you think about how data types are
designed. These questions are not intended to have a single ‘right’ answer, rather
you should satisfy yourself that you have adequately represented the types which
appear in your informal picture of the problem.
Exercises
14.33 It is decided to keep a record of vehicles which will use a particular car park.
Design an algebraic data type to represent them.
14.34 If you knew that the records of vehicles were to be used for comparative tests
of fuel efficiency, how would you modify your answer to the last question?
14.35 Discuss the data types you might use in a database of students’ marks for
classes and the like. Explain the design of any algebraic data types that you
use.
14.36 What data types might be used to represent the objects which can be drawn
using an interactive drawing program? To give yourself more of a challenge,
you might like to think about grouping of objects, multiple copies of objects,
and scaling.
Movable objects
We start by building a class of types whose members are geometrical objects in two
dimensions. The operations of the class are those to move the objects in various
different ways.
We now work through the definitions, which are illustrated in Figures 14.6 and
14.7. Some moves will be dictated by vectors, so we first define
and it shows the ways in which an object can be moved. First it can be moved by a
vector, as in the diagram below.
We can also reflect an object in the x-axis (the horizontal axis) or the y-axis (the
vertical), or rotate a figure through 180± around the origin (the point where the axes
meet). The default definition of rotate180 works by reflecting first in the y-axis and
then the x, as we did with the Picture type in Chapter 1.
We can now define a hierarchy of movable objects; first we have the Point,
data Point = Point Float Float
deriving Show
To make Point an instance of Movable we have to give definitions of move, reflectX
and reflectY over the Point type.
move (Vec v1 v2) (Point c1 c2) = Point (c1+v1) (c2+v2)
14.6. ALGEBRAIC TYPES AND TYPE CLASSES 365
Here we can see that the move is achieved by adding the components v1 and v2 to
the coordinates of the point. Reflection is given by changing the sign of one of the
coordinates
reflectX (Point c1 c2) = Point c1 (-c2)
reflectY (Point c1 c2) = Point (-c1) c2
For this instance we override the default definition of rotate180 by changing the
sign of both coordinates. This is a more efficient way of achieving the same trans-
formation than the default definition.
rotate180 (Point c1 c2) = Point (-c1) (-c2)
Using the type of points we can build figures:
data Figure = Line Point Point |
Circle Point Float
and in the instance declaration of Movable for Figure given in Figure 14.7 we use
the corresponding operations on Point; for example,
move v (Line p1 p2) = Line (move v p1) (move v p2)
move v (Circle p r) = Circle (move v p) r
This same approach works again when we consider a list of movable objects:
instance Movable a => Movable [a] where
move v = map (move v)
reflectX = map reflectX
366 CHAPTER 14. ALGEBRAIC TYPES
• The code is much easier to read: at each point we write move, rather than
movePoint, and so on.
• We can reuse definitions; the instance declaration for Movable [a] makes
lists of any sort of movable object movable themselves. This includes lists
of points and lists of figures. Without overloading we would not be able to
achieve this.
Named objects
Many forms of data contain some sort of name, a String which identifies the object
in question. What do we expect to be able to do with a value of such a type?
Exercises
14.37 A different way of combining the classes Named and Movable is to establish
the instance
14.38 Show that the method of the previous question can be used to combine in-
stances of any two classes.
14.39 The example in the final part of this section shows how we can combine an
arbitrary instance of the Movable class, a, with a particular instance of the
Named class, String. Show how it can be used to combine an arbitrary in-
stance of one class with a particular instance of another for any two classes
whatever.
14.40 Extend the collection of operations for moving objects to include scaling and
rotation by an arbitrary angle. This can be done by re-defining Movable or
by defining a class MovablePlus over the class Movable. Which approach is
preferable? Explain your answer.
14.41 Design a collection of classes to model bank accounts. These have different
forms: current, deposit and so on, as well as different levels of functionality.
Can you reuse the Named class here?
Trees
Structural induction over the type Tree of trees is stated as follows.
To prove the property P(tr) for all finite tr of type Tree t we have to do two things.
Nil case Prove P(Nil).
Node case Prove P(Node x tr1 tr2) for all x of type t
assuming that P(tr1) and P(tr2) hold already.
The advice of Chapter 9 about finding proofs can easily be carried over to the situa-
tion here. Now we give a representative example of a proof. We aim to prove for all
finite trees tr that
which states that if we map a function over a tree, and then collapse the result we
get the same result as collapsing before mapping over the list. The functions we use
are defined as follows
map f [] = [] (map.1)
map f (x:xs) = f x : map f xs (map.2)
The final step is given by the two induction hypotheses, that the result holds for the
two subtrees tr1 and tr2. The result (map++) is the theorem
and this finishes the proof in the Node case. As this is the second of the two cases,
the proof is complete.
To prove the property P(x) for all defined1 x of type Maybe t we have to do two
things:
Nothing case Prove P(Nothing).
Just case Prove P(Just y) for all defined y of type t.
Our example proof is that, for all defined values x of type Maybe Int,
maybe 2 abs x ∏ 0
1 When the type is not recursive, the induction principle gives a proof for all defined objects. An object
of this type is defined if it is Nothing, or Just y for a defined y.
14.7. REASONING ABOUT ALGEBRAIC TYPES 371
Proof The proof has two cases. In the first x is replaced by Nothing:
which gives the induction step, and therefore completes the proof.
This result shows that verification is possible for functions defined in a more
general way than primitive recursion.
Exercises
14.42 Prove that the function weather from Section 5.3 has the same behaviour as
when
14.43 Is it the case that the area of each Shape from Section 5.3 is non-negative? If
so, give a proof; if not, give an example which shows that it is not the case.
14.7. REASONING ABOUT ALGEBRAIC TYPES 373
size NilT = 0
size (Node x t1 t2) = 1 + size t1 + size t2
The next two exercises refer back to the exercises of Section 14.3.
14.46 Prove that the function twist has the property that
twist . twist = id
14.47 Explain the principle of structural induction for the type GTree. Formulate
and prove the equivalent of the theorem relating map, mapTree and collapse
for this type of trees.
Summary
Algebraic types sharpen our ability to model types in our programs: we have seen in
this chapter how simple, finite types like Temp can be defined, as well as the more
complex Either and recursive types. Many of these recursive types are varieties of
tree: we looked at numerical trees; elements of the type Expr can also be thought of
as trees representing the underlying structure of arithmetical expressions.
The type of lists gives a guiding example for various aspects of algebraic types.
• The definition of the type is recursive and polymorphic, and many polymor-
phic higher-order functions can be defined over lists – this carries over to the
various types of tree and the error type, Maybe, for example.
• There is a simple principle for reasoning over lists, structural induction, which
is the model for structural induction over algebraic types.
The chapter also gives guidelines for defining algebraic types. The definition can
be given in three parts: first the type name is identified, then the constructors are
named, and finally their component types are specified. As in other aspects of pro-
gram development, this separation of concerns assists the system developer to pro-
duce simple and correct solutions.
Having introduced algebraic data types we are able to give more substantial ex-
amples of classes and their instances. We can see that the overloading that classes
bring makes code both easier to read and more amenable to reuse; we can see in
374 CHAPTER 14. ALGEBRAIC TYPES
particular how software can be extended in a way that requires little modification to
the code.
In the chapters to come, algebraic types will be an integral part of the systems we
develop, and indeed in the next case study we exhibit various aspects of these types.
We shall also explore a different approach to types: abstract data types, and see how
this approach complements and contrasts with the use of algebraic data types.
Chapter 15
We use the case study in this chapter as a vehicle to illustrate many of the features
of the previous chapters – polymorphism, algebraic types and program design – and
to illustrate the module system of Haskell, which is discussed first.
Module headers
Each module is named, so an example named Ant might be
375
376 CHAPTER 15. CASE STUDY: HUFFMAN CODES
Note that the definitions all begin in the column under the keyword module; it is
safest to make this the leftmost column of the file.
We also assume that a module Foo will live in the file Foo.hs; GHC allows mod-
ule and file names to be different, but we recommend that you keep them the same.
Importing a module
The basic operation on modules is to import one into another, so in defining Bee
we might say
import Ant
beeKeeper = ...
This means that the visible definitions from Ant can be used in Bee. By default the
visible definitions in a module are those which appear in the module itself. If we
define
import Bee
the definitions of Ants and anteater will not be visible in Cow. They can be made
visible either by importing Ant explicitly, or by using the export controls discussed
below to modify exactly what is exported from Bee.
Export controls
As we explained when import was introduced, the default is that all top-level defi-
nitions of a module are exported.
• This may be too much: we might wish not to export some auxiliary functions,
such as the shunt function below
We can control what is exported by following the name of the module with a list of
what is to be exported. For instance, we say in the case of Bee
The list contains names of defined objects, such as beeKeeper, and also data types
like Ants. In the latter case we follow the type name with (..) to indicate that the
constructors of the type are exported with the type itself; if this is omitted, then the
type acts like an abstract data type, which we investigate further in the next chapter.
The (..) is not necessary for a type definition.
Such a list works on a definition-by-definition basis; we can also state that all the
definitions in a module are to be exported, as in
or equivalently
where preceding the name of a module by the keyword module is shorthand for all
the names defined within the module. The simple header
is therefore equivalent to
Import controls
We can control how objects are to be imported, just as we can control their export.
We do this by following the import statement with a list of objects, types or classes.
For instance, if we choose not to import anteater from Ant we can write
378 CHAPTER 15. CASE STUDY: HUFFMAN CODES
stating that we want just the type Ants; we can alternatively say which names we
wish to hide:
Suppose that in our module we have a definition of bear, and also there is an ob-
ject named bear in the module Ant. How can we gain access to both definitions?
The answer is that we use the qualified name Ant.bear for the imported object,
reserving bear for the locally defined one.
A qualified name is built from the name of a module and the name of an object
in that module, separated by a full stop. Note that there should be no white space
between the ‘.’ and the two names, so as to avoid confusion with the composition
operator. To use qualified names we should make the import like this:
In the qualified case we can also state which particular items are to be imported or
hidden, just as in the unqualified case above. It is possible to use a local name for an
imported module, as in
which gives the local name Ant to the imported module Insect.
so that we can give our own definition of the name words. If we import Eagle into
another module, this module will also have explicitly to hide the import of words
from the Prelude if conflicting definitions are to be avoided, and so we see that a
re-definition of a Prelude function cannot be done ‘invisibly’, as it were.
If we also wish to have access to the original definition of words we can make a
qualified import of the prelude,
and use the original words by writing its qualified name Prelude.words.
Further details
Further information about the Haskell module system can be found in the language
report (Marlow 2010); note that some of the details will be different in particular
implementations.
Exercises
15.1 Can you get the effect of export controls using import? Can you get the ef-
fect of the qualifications of import using export controls? Discuss why both
directives are included in the language.
15.2 Explain why you think it is the default that imported definitions are not them-
selves exported.
15.3 It is proposed to add the following option to the module export control and
the import statement. If the item -module Dog appears, then none of the
definitions in the module Dog is exported or imported. Discuss the advantages
and disadvantages of this proposal. How would you achieve the effect of this
feature in the existing Haskell module system?
module (at least initially), rather than starting by modifying the whole of the system
as a single unit.
How should we begin to design a system as a collection of modules? The pieces
of advice which follow are aimed to make modification as straightforward as possi-
ble.
We have also mentioned design for reuse, particularly in the context of polymorphic
types and higher-order functions. The module will be the unit of reuse, and a library
will be accessed by means of an import statement. Similar principles apply to the
design of libraries. Each library should have a clearly defined purpose, like imple-
menting a type together with basic operations over the type. In addition, we can say
that
The advice here might seem dry – what has been said is illustrated in the case study
which follows. In the next chapter we will return to the idea of information hiding
when we meet abstract data types. In the remainder of this chapter we examine the
case study of Huffman coding, the foundations of which we explore now.
b t
We can see this as giving codes for the letters a, b and t by looking at the routes
taken to reach the letters. For example, to get to b, we go right at the top node, and
left at the next:
b t
which gives b the code RL. Similarly, L codes a, and RR the letter t.
The codes given by trees are prefix codes; in these codes no code for a letter is the
start (or prefix) of the code for another. This is because no route to a leaf of the tree
can be the start of the route to another leaf. For more information about Huffman
codes and a wealth of general material on algorithms, see Cormen, Leiserson, and
Rivest (1990).
A message is also decoded using the tree. Consider the message RLLRRRRLRR. To
decode we follow the route through the tree given, moving right then left, to give the
letter b,
a a
a
b t b t
b t
where we have shown under each tree the sequence of bits remaining to be decoded.
Continuing again from the top, we have the codes for a then t,
382 CHAPTER 15. CASE STUDY: HUFFMAN CODES
a a a
b t b t b t
a b
the coded message becomes RRRLLLRLL, a nine-bit coding. A Huffman code is built
so that the most frequent letters have the shortest sequences of code bits, and the
less frequent have more ‘expensive’ code sequences, justified by the rarity of their
occurrence; Morse code is an example of a Huffman code in common use.
The remainder of the chapter explores the implementation of Huffman coding,
illustrating the module system of Haskell.
Exercises
15.4 What is the coding of the message battat using the following tree?
a t
Compare the length of the coding with the others given earlier.
15.5 Using the first coding tree, decode the coded message RLLRLRLLRR. Which
tree would you expect to give the best coding of the message? Check your
answer by trying the three possibilities.
15.4. IMPLEMENTATION – I 383
15.4 Implementation – I
We now begin to implement the Huffman coding and decoding, in a series of Haskell
modules. The overall structure of the system we develop is illustrated at the end of
the chapter in Figure 15.5.
As earlier, we first develop the types used in the system.
and in the translation we will convert the Huffman tree to a table for ease of coding.
The Huffman trees themselves carry characters at the leaves. We shall see presently
that during their formation we also use information about the frequency with which
each character appears; hence the inclusion of integers both at the leaves and at the
internal nodes.
The file containing the module is illustrated in Figure 15.1. The name of the file, with
an indication of its purpose, is listed at the start of the file; each of the definitions is
preceded by a comment as to its purpose.
Note that we have given a full description of what is exported by the module,
by listing the items after the module name. For the data types which are exported,
Tree and Bit, the constructors are exported explicitly; this could also be done by
following their names with (..). This interface information could have been omit-
ted, but we include it here as useful documentation of the interface to the module.
We have chosen to list the names imported here; the statement import Types would
have the same effect, but would lose the extra documentation.
The purpose of the module is to define functions to code and decode messages:
we export only these, and not the auxiliary function(s) which may be used in their
definition. Our module therefore has the header
-- Types.hs
--
-- The types used in the Huffman coding example.
It is interesting to see that the function level definition here gives an exact imple-
mentation of the description which precedes it; using partial application and func-
tion composition has made the definition clearer.
We now define lookupTable, which is a standard function to look up the value
corresponding to a ‘key’ in a table.
• If we are at an internal Node, we choose the sub-tree dictated by the first bit of
the code.
• If at a leaf, we read off the character found, and then begin to decode the re-
mainder of the code at the top of the tree tr.
decodeMessage tr
= decodeByt tr
where
decodeByt (Node n t1 t2) (L:rest)
= decodeByt t1 rest
decodeByt (Node n t1 t2) (R:rest)
= decodeByt t2 rest
decodeByt (Leaf c n) rest
= c : decodeByt tr rest
decodeByt t [] = []
The locally defined function is called decodeByt because it decodes ‘by t’.
The first coding tree and example message of Section 15.3 can be given by
386 CHAPTER 15. CASE STUDY: HUFFMAN CODES
decodeMessage exam1
mess1
; decodeByt exam1
mess1
; decodeByt exam1
[R,L,L,R,R,R,R,L,R,R]
; decodeByt (Node
0 (Leaf ’b’ 0) (Leaf ’t’ 0))
[L,L,R,R,R,R,L,R,R]
; decodeByt (Leaf ’b’ 0) [L,R,R,R,R,L,R,R]
; ’b’ : decodeByt exam1 [L,R,R,R,R,L,R,R]
; ’b’ : decodeByt (Leaf ’a’ 0) [R,R,R,R,L,R,R]
; ’b’ : ’a’ : decodeByt exam1 [R,R,R,R,L,R,R]
Before looking at the implementation any further, we look at how to construct the
Huffman coding tree, given a text.
Exercises
give a calculation of
• We first find the frequencies of the individual letters, in this case giving
[(’b’,1),(’a’,2),(’t’,3)]
• The main idea of the translation is to build the tree by taking the two charac-
ters occurring least frequently, and making a single character (or tree) of them.
This process is repeated until a single tree results; the steps which follow give
this process in more detail.
• Each of (’b’,1), . . . is turned into a tree, giving the list of trees
b a
15.6 Design
Implementing the system will involve us in designing various modules to perform
the stages given above. We start by deciding what the modules will be and the func-
tions that they will implement. This is the equivalent at the larger scale of divide and
conquer; we separate the problem into manageable portions, which can be solved
separately, and which are put together using the import and module statements.
We design these interfaces before implementing the functions.
The three stages of conversion are summarized in Figure 15.2, which shows the
module directives of the three component files. We have added as comments the
types of objects to be exported, so that these directives contain enough informa-
tion for the exported functions in the files to be used without knowing how they are
defined.
In fact the component functions frequency and makeTree will never be used
separately, and so we compose them in the module MakeCode.hs when bringing
the three files together. This is given in Figure 15.3.
Our next task is to implement each module in full and we turn to that now.
388 CHAPTER 15. CASE STUDY: HUFFMAN CODES
Frequency.hs:
MakeTree.hs:
CodeTable.hs:
--
-- MakeCode.hs
--
-- Huffman coding in Haskell.
--
import Types
import Frequency ( frequency )
import MakeTree ( makeTree )
import CodeTable ( codeTable )
15.7 Implementation – II
In this section we discuss in turn the three implementation modules.
[(’b’,1),(’a’,1),(’t’,1),(’t’,1),(’a’,1),(’t’,1)]
• Next, we sort the list on the characters, bringing together the counts of equal
characters.
[(’a’,2),(’b’,1),(’t’,3)]
• Finally, we sort the list into increasing frequency order, to give the list above.
The function uses two different sorts – one on character, one on frequency – to
achieve its result. Is there any way we can define a single sorting function to per-
form both sorts?
We can give a general merge sort function, which works by merging, in order,
the results of sorting the front and rear halves of the list.
mergeSort merge xs
| length xs < 2 = xs
| otherwise
= merge (mergeSort merge first)
(mergeSort merge second)
where
first = take half xs
second = drop half xs
half = (length xs) ‘div‘ 2
The first argument to mergeSort is the merging function, which takes two sorted
lists and merges their contents in order. It is by making this operation a parameter
that the mergeSort function becomes reusable.
In sorting the characters, we amalgamate entries for the same character
alphaMerge xs [] = xs
alphaMerge [] ys = ys
390 CHAPTER 15. CASE STUDY: HUFFMAN CODES
freqMerge xs [] = xs
freqMerge [] ys = ys
freqMerge ((p,n):xs) ((q,m):ys)
| (n<m || (n==m && p<q))
= (p,n) : freqMerge xs ((q,m):ys)
| otherwise
= (q,m) : freqMerge ((p,n):xs) ys
We can now give the top-level definition of frequency
frequency :: [Char] -> [ (Char,Int) ]
frequency
= mergeSort freqMerge . mergeSort alphaMerge . map start
where
start ch = (ch,1)
which we can see is a direct combination of the three stages listed in the informal
description of the algorithm.
Note that of all the functions defined in this module, only frequency is ex-
ported.
makeCodes [t] = t
makeCodes ts = makeCodes (amalgamate ts)
How are trees amalgamated? We have to pair together the first two trees in the list
(since the list is kept in ascending order of frequency) and then insert the result in
the list preserving the frequency order. Working top-down, we have
value (Leaf _ n) = n
value (Node n _ _) = n
The definition of insTree, which is similar to that used in an insertion sort, is left as
an exercise. Again, the definition of the exported function uses various others whose
definitions are not visible to the ‘outside world’.
whose first argument is the ‘path so far’ into the tree. The definition is
convert cd (Leaf c n)
= [(c,cd)]
convert cd (Node n t1 t2)
= (convert (cd++[L]) t1) ++ (convert (cd++[R]) t2)
The codeTable function is given by starting the conversion with an empty code
string
392 CHAPTER 15. CASE STUDY: HUFFMAN CODES
is also possible to write unit tests, which check that a particular function or module
has the required functionality.
For example, we might look at the module Frequency.hs, and in particular at
the functions designed to merge or sort their arguments. A QuickCheck property
embodying a unit test for these would include
prop_mergeSort xs =
sorted (mergeSort merge xs)
where sorted expresses that its argument is sorted into ascending order and merge
394 CHAPTER 15. CASE STUDY: HUFFMAN CODES
Main
decodeMessage codeTable
codeMessage codes
Coding MakeCode
Types
Exercises
15.8 Give a definition of merge sort which uses the built-in ordering ‘<=’. What is
its type?
15.9 Modifying your previous answer if necessary, give a version of merge sort which
removes duplicate entries.
15.10 Give a version of merge sort which takes an ordering function as a parameter:
makeTree [(’b’,2),(’a’,2),(’t’,3),(’e’,4)]
which give printable versions of Huffman trees and code tables. One general
way of printing trees is to use indentation to indicate the structure. Schemati-
cally, this looks like
15.14 Define a QuickCheck property to test coding and decoding: specifically this
should code a string and decode it, and compare the result with the original.
You may need to restrict the strings over which the test is made.
15.15 Define sorted so that it checks whether its argument is sorted into ascend-
ing order and define merge which will merge two ordered lists in order. Using
these check whether the property prop_mergeSort :: [Int] -> Bool, de-
fined earlier, holds.
15.16 Write QuickCheck unit tests for other functions in Frequency.hs and other
modules of the Hufmann coding system.
15.17 You may find that it is necessary to restrict the domain over which some prop-
erties hold in order for them to QuickCheck successfully: can you re-define
the functions involved so that the properties hold for all randomly generated
inputs.
15.18 The correctness property for the Huffman system formulated earlier states
that decode.code is the identity. Would you expect that code.decode is the
identity: if so, over what domain; if not, can you give a sub-domain over which
you wold expect it to hold?
Summary
When writing a program of any size, we need to divide up the work in a sensible
way. The Haskell module system allows one script to be included in another. At the
boundary, it is possible to control exactly which definitions are exported from one
module and imported into another.
We gave a number of guidelines for the design of a program into its constituent
modules. The most important advice is to make each module perform one clearly
defined task, and for only as much information as is needed to be exported – the
396 CHAPTER 15. CASE STUDY: HUFFMAN CODES
principle of information hiding. This principle is extended in the next chapter when
we examine abstract data types.
The design principles were put into practice in the Huffman coding example. In
particular, it was shown for the file MakeCode.hs and its three sub-modules that
design can begin with the design of modules and their interfaces – that is the defi-
nitions (and their types) which are exported. Thus the design process starts before
any implementation takes place.
Chapter 16
The Haskell module system allows definitions of functions and other objects to be
hidden when one module is imported into another. Those definitions hidden are
only of use in defining the exported functions, and hiding them makes clearer the
exact interface between the two modules: only those features of the module which
are needed will be visible.
This chapter shows that information hiding is equally applicable for types, giv-
ing what are known as abstract data types, or ADTs. We explain the abstract data
type mechanism here, as well as providing a number of examples of ADTs, includ-
ing queues, sets, relations and the fundamental types belonging to a simulation case
study.
397
398 CHAPTER 16. ABSTRACT DATA TYPES
User
initial :: Store
value :: Store -> Var -> Integer
update :: Store -> Var -> Integer -> Store
Implementor
but each model allows more than that: we can, for instance, reverse a list, or com-
pose a function with others. In using the type Store we intend only to use the three
operations given, but it is always possible to use the model in unintended ways.
How can we give a better model of a store? The answer is to define a type which
only has the operations initial, value and update, so that we cannot abuse the
representation. We therefore hide the information about how the type is actually
implemented, and only allow the operations (StoreSig) to manipulate objects of
the type.
When we provide a limited interface to a type by means of a specified set of oper-
ations we call the type an abstract data type (or ADT). Since the ‘concrete’ type itself
is no longer accessible and we may only access the type by means of the operations
provided, these operations give a more ‘abstract’ view of the type.
Figure 16.1 illustrates the situation, and suggests that as well as giving a natural
representation of the type of stores, there are two other benefits of type abstraction.
• We can modify the implementation of the Store without having any effect on
the user. Contrast this with the situation where the implementation is visible
to the user. In particular, if the implementation is an algebraic type then any
change in the implementation will mean that all definitions that use pattern
matching will have to be changed. These will include not just those in the
signature, but also any user-defined functions which use pattern matching.
We shall see both aspects illustrated in the sections to come; first we look at the
details of the Haskell abstract data type mechanism.
16.2. THE HASKELL ABSTRACT DATA TYPE MECHANISM 399
The initial store, init, is represented by an empty list; the value of v is looked up
by finding the first pair (n,v) in the list, and the store is updated by adding a new
(Integer,Var) at the front of the list.
These functions then have to be converted to work over the type Store, so that
arguments and results are of the form Store xs with xs::[(Integer,Var)]. The
definitions become
initial :: Store
initial = Store []
where we can see that the pattern of the definitions is similar, except that we have
to ‘unwrap’ arguments of the form (Store sto) on the left-hand side, and ‘wrap
up’ results using Store on the right-hand side. We look at a general mechanism for
‘wrapping up’ functions in the example of the Set ADT in Section 16.8.
What happens if we try to break the abstraction barrier and deal with a Store as
having the form (Store xs)? On typing
initial == Store []
The fact that initial is indeed implemented as Store [] is irrelevant, since the
implementation is not in scope outside the Store module.
which has the same effect as declaring a data type with one unary constructor but
which is implemented in a more efficient fashion.
16.2. THE HASKELL ABSTRACT DATA TYPE MECHANISM 401
Naming newtypes
Another possible way of implementing the type would be to say
using different names for the type and its constructor. Although this makes it clear
whether we are talking about the type or the constructor, we prefer to use a single
name within this newtype, as otherwise we need to choose two different – but re-
lated – names.
Moreover, in practice, when we use Store we will always be able to tell which use –
type name or constructor – is meant. Finally, this convention is in widespread use
in the Haskell developer community, and so it’s helpful to be aware of the fact.
Note, however, that once declared, these instances cannot be hidden, so that even
though they are not named in the export list, the functions over Store which are
defined by means of these instance declarations will be available whenever the
module Store is imported. Of course, we can choose not to declare these instances,
and so not to provide an equality or a show function over Stores.
Stores as functions
A different implementation of Store is given by the type of functions from variables
to integers.
initial :: Store
initial = Store (\v -> 0)
Testing ADTs
Suppose that we have implemented a store as a list of integer-variable pairs. We can
inspect the result of updating a store, which will be a new list, and check that it has
the properties that we would expect. For example, we might take the difference of
the lists before and after the update.
If we implement queues as an ADT, we can’t look directly at the underlying im-
plementation; instead we have to write properties using the interface functions, here
initial, value and update. We can start by saying what happens if we look up a
value in the initial store:
prop_Initial :: Char -> Bool
prop_Initial ch =
value initial ch == 0
What can we say about the effect of an update on a store? First, if perform an update
for the variable ch and then look up the value, we would expect to see the new value:
prop_Update1 :: Char -> Integer -> Store -> Bool
prop_Update1 ch int st =
value (update st ch int) ch == int
Finally, what if we look up the value of another variable after this update? Its value
should be the same as it was before:
prop_Update2 :: Char -> Char -> Integer -> Store -> Bool
Because the tests are written in terms of the interface only, if we were to change the
implementation then the tests could still be applied, assuming that we have the right
QuickCheck definitions in place.
Exercises
16.1 Give an implementation of Store using lists whose entries are ordered ac-
cording to the variable names. Discuss why this might be preferable to the
original list implementation, and also its disadvantages, if any.
16.2 For the implementation of Store as a list type [(Integer,Var)], give a defi-
nition of equality which equates any two stores which give the same values to
each variable. Can this operation be defined for the second implementation?
If not, give a modification of the implementation which allows it to be defined.
16.3 In this question you should use the type Maybe a. Suppose it is an error to
look up the value of a variable which does not have a value in the given store.
Explain how you would modify both the signature of Store and the two im-
plementations.
16.4 Rather than giving an error when looking up a variable which does not have a
value in the particular store, extend the signature to provide a test of whether
a variable has a value in a given store, and explain how would modify the two
implementations to define the test.
so that setAll n is the store where every variable has the value n. Can you do
this for both the example implementations? Show how if you can, and explain
why, if not.
16.6 Design an ADT for the library database, first examined in Chapter 5.
16.3 Queues
A queue is a ‘first in, first out’ structure. If first Flo and then Eddie joins an initially
empty queue, the first person to leave will be Flo. As an abstract data type, we expect
to be able to add items and remove items as well as there being an empty queue.
module Queue
( Queue ,
emptyQ , -- Queue a
isEmptyQ , -- Queue a -> Bool
addQ , -- a -> Queue a -> Queue a
remQ -- Queue a -> ( a , Queue a )
) where
404 CHAPTER 16. ABSTRACT DATA TYPES
The function remQ returns a pair – the item removed together with the part of the
queue that remains – if there are any items in the queue. If not, the standard function
error is called.
A list can be used to model a queue: we add to the end of the list, and remove
from the front, giving
emptyQ :: Queue a
emptyQ = Queue []
As (@) patterns
The definition of remQ uses an aspect of pattern matching which we have not seen
so far. We use the pattern q@(Queue xs), where we can read ‘@’ as ‘as’, to match
the input. The variable q matches the whole input, while it is also matched against
Queue xs, so that xs gives us access to the list from which it is built. This means
that we can refer directly to the whole input and to its components in the definition.
Without this, the alternative would be
In implementing queues, rather than adding elements at the end of the list, we could
choose to add them at the beginning of the list. This leaves emptyQ and isEmptyQ
unchanged, and gives
7 5 3 2
remQ
7 5 3 2
addQ 0
5 0 3 2
remQ
5 0 3 2
remQ
2 3 0
where the built-in functions last and init take the last element and the remainder
of a list.
Although we have not said exactly how to calculate the cost of evaluation (a topic
we take up in Chapter 20), we can see that in each implementation one of the op-
erations is ‘cheap’ and the other is ‘expensive’. The ‘cheap’ functions – remQ in the
first implementation and addQ in the second – can be evaluated in one step, while
in both cases the ‘expensive’ function will have to run along a list xs one step per
element, and so will be costly if the list is long.
Is there any way of making both operations ‘cheap’? The idea is to make the
queue out of two lists, so that both adding and removing an element can take place
at the head of a list. The process is illustrated in Figure 16.2, which represents
a number of queues. Initially the queue containing the elements 7, 5, 2 and 3 is
shown: here 7 is the oldest element in the queue and 3 the most recent addition.
Subsequently we see the effect of removing an element, adding the element 0, and
removing two further elements. In each case the queue is represented by two lists,
each being shown with its head at the left-hand side.
The function remQ removes elements from the head of the left-hand list, and
addQ adds elements to the head of the right. This works until the left-hand list is
empty, when the elements of the right-hand queue have to be transferred to the left,
reversing their order.
406 CHAPTER 16. ABSTRACT DATA TYPES
emptyQ :: Queue a
emptyQ = Queue [] []
As we commented for the Store types, the behaviour of this implementation will be
indistinguishable from the first two, as far as the operations of the abstract data type
are concerned. On the other hand, the implementation will be substantially more
efficient than the single list implementations, as we explained above. A thorough
examination of recent work on the efficient implementation of data structures in
functional languages can be found in Okasaki (1998).
Using newtype
Why didn’t we use a newtype in the definition of Queue? The reason is that a
newtype must have a single argument, and Queue has two; we could pair the ar-
guments, and then use a newtype. We leave this as an exercise for the reader.
Exercises
"abcde" ++ "f"
init "abcdef"
last "abcdef"
where
16.8 Explain the behaviour of the three queue models if you are asked to perform
the following sequence of queue operations: add 2, add 1, remove item, add 3,
remove item, add 1, add 4, remove item, remove item.
16.9 Define QuickCheck properties which will test the queue implementations given
here. We will show how to define the appropriate generators needed to per-
form the tests in Chapter 19.
16.11 A unique queue can contain only one occurrence of each entry (the one to
arrive earliest). Give a signature for the ADT of these queues, and an imple-
mentation of the ADT.
16.12 Each element of a priority queue has a numerical priority. When an element
is removed, it will be of the highest priority in the queue. If there is more than
one of these, the earliest to arrive is chosen. Give a signature and implemen-
tation of the ADT of priority queues.
16.13 [Harder] Examine how priority queues could be used to implement the Huff-
man coding system in Chapter 15.
16.4 Design
This section examines the design of Haskell abstract data types, and how the pres-
ence of this mechanism affects design in general.
General principles
In building a system, the choice of types is fundamental, and affects the subsequent
design and implementation profoundly. If we use abstract data types at an early
stage we hope to find ‘natural’ representations of the types occurring in the problem.
Designing the abstract data types is a three-stage process.
How do we decide what should go in the signature? This is the $64,000 question, of
course, but there are some general questions we can ask of any abstract data type
signature.
408 CHAPTER 16. ABSTRACT DATA TYPES
• Can we create objects of the type? For instance, in the Queue a type, we have
the object emptyQ, and in a type of sets, we might give a function taking an
element to the ‘singleton’ set containing that element alone. If there are no
such objects or functions, something is wrong!
• Can we check what sort of object we have? In a tree ADT we might want to
check whether we have a leaf or a node, for instance.
• Can we extract the components of objects, if we so require? Can we take the
head of a Queue a, say?
• Can we transform objects: can we reverse a list, perhaps, or add an item to a
queue?
• Can we combine objects? We might want to be able to join together two trees,
for example.
• Can we collapse objects? Can we take the sum of a numerical list, or find the
size of an object, say?
Not all these questions are appropriate in every case, but the majority of operations
we perform on types fall into one of these categories. All the operations in the fol-
lowing signature for binary trees can be so classified, for instance.
module Tree
(Tree,
nil, -- Tree a
isNil, -- Tree a -> Bool
isNode, -- Tree a -> Bool
leftSub, -- Tree a -> Tree a
rightSub, -- Tree a -> Tree a
treeVal, -- Tree a -> a
insTree, -- Ord a => a -> Tree a -> Tree a
delete, -- Ord a => a -> Tree a -> Tree a
minTree -- Ord a => Tree a -> Maybe a
) where
Other functions might be included in the signature; in the case of Tree a we might
want to include the size function. This function can be defined using the other
operations.
Exercises
16.14 Are all the operations in the Tree a signature necessary? Identify those which
can be implemented using the other operations of the signature.
16.15 Design a signature for an abstract type of library databases, as first introduced
in Chapter 5.
16.16 Design a signature for an abstract type of indexes, as examined in Section 12.5.
16.5 Simulation
We first introduced the simulation example in Section 14.5, where we designed the
algebraic types Inmess and Outmess. Let us suppose, for ease of exposition, that the
system time is measured in minutes.
The Inmess No signals no arrival, while Yes 34 12 signals the arrival of a cus-
tomer at the 34th minute, who will need 12 minutes to be served.
The Outmess Discharge 34 27 12 signals that the person arriving at time 34
waited 27 minutes before receiving their 12 minutes of service.
Our aim in this section is to design the ADTs for a simple simulation of queueing.
We start by looking at a single queue. Working through the stages, we will call the
type QueueState, and it can be described thus.
There are two main operations on a queue. The first is to add a new
item, an Inmess, to the queue. The second is to process the queue by
a one-minute step; the effect of this is to give one minute’s further pro-
cessing to the item at the head of the queue (if there is such a thing). Two
outcomes are possible: the item might have its processing completed,
in which case an Outmess is generated, or further processing may be
needed.
Other items we need are an empty queue, an indication of the length of
a queue and a test of whether a queue is empty.
module QueueState
( QueueState ,
addMessage, -- Inmess -> QueueState -> QueueState
queueStep, -- QueueState -> ( QueueState , [Outmess] )
queueStart, -- QueueState
queueLength, -- QueueState -> Int
queueEmpty -- QueueState -> Bool
) where
The queueStep function returns a pair: the QueueState after a step of processing,
and a list of Outmess. A list is used, rather than a single Outmess, so that in the case
of no output an empty list can be returned.
410 CHAPTER 16. ABSTRACT DATA TYPES
The QueueState type allows us to model a situation in which all customers are
served by a single processor (or bank clerk). How can we model the case where
there is more than one queue? We call this a server and it is to be modelled by the
ServerState ADT.
As a signature, we have
module ServerState
( ServerState ,
addToQueue, -- Int -> Inmess -> ServerState -> ServerState
serverStep, -- ServerState -> ( ServerState , [Outmess] )
simulationStep, -- ServerState -> Inmess -> ( ServerState ,
[Outmess] )
serverStart, -- ServerState
serverSize, -- ServerState -> Int
shortestQueue -- ServerState -> Int
) where
In the next section we explore how to implement these two abstract data types. It
is important to realize that users of the ADTs can begin to do their programming
now: all the information that they need to know is contained in the signature of the
abstract data type.
Exercises
16.17 Are there redundant operations in the signatures of the ADTs QueueState and
ServerState?
16.18 Design a signature for round-robin simulation, in which allocation of the first
item is to queue 0, the second to queue 1, and so on, starting again at 0 after
the final queue has had an element assigned to it.
16.6. IMPLEMENTING THE SIMULATION 411
The queue
In the previous section, we designed the interfaces for the ADT; how do we pro-
ceed with implementation? First we ought to look again at the description of the
QueueState type. What information does this imply the type should contain?
where the first field gives the current time, the second the service time so far for the
item currently being processed, and the third the queue itself. Now we look at the
operations one by one. To add a message, it is put at the end of the list of messages.
In the first case, when the service time so far (servSoFar) is smaller than is required
(serv), processing is not complete. We therefore add one to the time, and the service
so far, and produce no output message.
If processing is complete – which is the otherwise case – the new state of the
queue is QS (time+1) 0 inRest. In this state the time is advanced by one, pro-
cessing time is set to zero and the head item in the list is removed. An output mes-
sage is also produced in which the waiting time is given by subtracting the service
and arrival times from the current time.
If there is nothing to process, then we simply have to advance the current time
by one, and produce no output.
queueStep (QS time serv []) = (QS (time+1) serv [] , [])
Note that the case of an input message No is not handled here since these messages
are filtered out by the server; this is discussed below.
The three other functions are given by
queueStart :: QueueState
queueStart = QS 0 0 []
The server
The server consists of a collection of queues, accessed by integers from 0; we choose
to use a list of queues.
newtype ServerState = SS [QueueState]
deriving (Eq, Show)
Note that the implementation of this ADT builds on another ADT; this is not un-
usual. Now we take the functions in turn.
Adding an element to a queue uses the function addMessage from the QueueState
abstract type.
addToQueue :: Int -> Inmess -> ServerState -> ServerState
A step of the server is given by making a step in each of the constituent queues, and
concatenating together the output messages they produce.
simulationStep
:: ServerState -> Inmess -> ( ServerState , [Outmess] )
simulationStep servSt im
= (addNewObject im servSt1 , outmess)
where
(servSt1 , outmess) = serverStep servSt
Adding the message to the shortest queue is done by addNewObject, which is not
in the signature. The reason for this is that it can be defined using the operations
addToQueue and shortestQueue.
addNewObject :: Inmess -> ServerState -> ServerState
serverStart :: ServerState
serverStart = SS (replicate numQueues queueStart)
where numQueues is a constant to be defined, and the standard function replicate
returns a list of n copies of x when applied thus: replicate n x.
This concludes the implementation of the two simulation ADTs. The example is in-
tended to show the merit of designing in stages. First we gave an informal descrip-
tion of the operations on the types, then a description of their signature, and finally
an implementation. Dividing the problem up in this way makes each stage easier to
solve.
The example also shows that types can be implemented independently: since
ServerState uses only the abstract data type operations over QueueState, we can
reimplement QueueState without affecting the server state at all.
Exercises
16.20 If we let
serverStep serverSt1
simulationStep (Yes 13 10) serverSt1
16.21 Define QuickCheck properties which will test the simulation system. We will
show how to define the appropriate generators needed to perform the tests in
Chapter 19.
16.22 Explain why we cannot use the function type (Int -> QueueState) as the
representation type of ServerState. Design an extension of this type which
will represent the server state, and implement the functions of the signature
over this type.
16.23 Given the implementations of the ADTs from this section, is your answer to
the question of whether there are redundant operations in the signatures of
queues and servers any different?
16.7. SEARCH TREES 415
16.24 If you have not done so already, design a signature for round-robin simulation,
in which allocation of the first item is to queue 0, the second to queue 1, and
so on.
16.25 Give an implementation of the round-robin simulation which uses the ServerState
ADT.
3 7 7 2 9
2 9 3 2 9 3
and the standard operations to discriminate between different sorts of tree and to
extract components are defined by
nil :: Tree a
nil = Nil
Figure 16.4 contains the definitions of the insertion, deletion and join functions. The
function join is used to join two trees with the property that all elements in the left
are smaller than all in the right; that will be the case for the call in delete where it
16.7. SEARCH TREES 417
minTree t
| isNil t = Nothing
| isNil t1 = Just v
| otherwise = minTree t1
where
t1 = leftSub t
v = treeVal t
join t1 t2
= Node mini t1 newt
where
(Just mini) = minTree t2
newt = delete mini t2
2 9 2 9 2 9 8
8 8 8 2 9
is used. It is not exported, as it can break the ordered property of search trees if it is
applied to an arbitrary pair of search trees.
Note that the types of insTree, delete, minTree and join contain the context
Ord a. Recall from Chapter 13 that this constraint means that these functions can
only be used over types which carry an ordering operation, <=. It is easy to see from
the definitions of these functions that they do indeed use the ordering, and given
the definition of search trees it is unsurprising that we use an ordering in these op-
erations. Now we look at the definitions in Figure 16.4 in turn.
Inserting an element which is already present has no effect, while inserting an
element smaller (larger) than the value at the root causes it to be inserted in the left
(right) subtree. Figure 16.3 shows 3 being inserted in the tree
(Node 7 (Node 2 Nil Nil) (Node 9 Nil Nil))
to give
(Node 7 (Node 2 Nil (Node 3 Nil Nil)) (Node 9 Nil Nil))
Deletion is straightforward when the value is smaller (larger) than the value at
the root node: the deletion is made in the left (right) sub-tree. If the value to be
deleted lies at the root, deletion is again simple if either sub-tree is Nil: the other
sub-tree is returned. The problem comes when both sub-trees are non-Nil. In this
case, the two sub-trees have to be joined together, keeping the ordering intact.
To join two non-Nil trees t1 and t2, where it is assumed that t1 is smaller than
t2, we pick the minimum element, mini, of t2 to be the value at the root. The left
sub-tree is t1, and the right is given by deleting mini from t2. Figure 16.5 shows the
deletion of 7 from
(Node 7 (Node 2 Nil Nil) (Node 9 (Node 8 Nil Nil) Nil))
to give
(Node 8 (Node 2 Nil Nil) (Node 9 Nil Nil))
The minTree function returns a value of type Maybe a, since a Nil tree has no min-
imum. The Just constructor therefore has to be removed in the where clause of
join.
16.7. SEARCH TREES 419
indexT n t (indexT)
| isNil t = error "indexT"
| n < st1 = indexT n t1
| n == st1 = v
| otherwise = indexT (n-st1-1) t2
where
v = treeVal t
t1 = leftSub t
t2 = rightSub t
st1 = size t1
where the size is given by
• We will have to redefine all the operations in the signature, since they access
the implementation type, and this has changed. For example, the insertion
function has the new definition
size Nil = 0
size (Node _ n _ _) = n
Exercises
16.27 Explain how you would test the implementations of the functions over search
trees. You might need to augment the signature of the type with a function to
print a tree.
16.28 Define QuickCheck properties which will test the implementation of search
trees. We will show how to define the appropriate generators needed to per-
form the tests in Chapter 19.
The successor of v in a tree t is the smallest value in t larger than v, while the
closest value to v in a numerical tree t is a value in t which has the smallest
difference from v. You can assume that closest is always called on a non-Nil
tree, so always returns an answer.
16.30 Redefine the functions of the Tree a signature over the Stree implementa-
tion type.
16.31 To speed up the calculation of maxTree and other functions, you could imag-
ine storing the maximum and minimum of the sub-tree at each node. Rede-
fine the functions of the signature to manipulate these maxima and minima,
and redefine the functions maxTree, minTree and successor to make use of
this extra information stored in the trees.
16.8. SETS 421
16.32 You are asked to implement search trees with a count of the number of times
an element occurs. How would this affect the signature of the type? How
would you implement the operations? How much of the previously written
implementation could be re-used?
16.33 Using a modified version of search trees instead of lists, reimplement the in-
dexing software of Section 12.5.
Tree a b c
so that entries at each node contain an item of type a, on which the tree is
ordered, and an item of type b, which might be something like the count, or a
list of index entries.
On inserting an element, information of type c is given (a single index entry
in that example); this information has to be combined with the information
already present. The method of combination can be a functional parameter.
There also needs to be a function to describe the way in which information is
transformed at deletion.
As a test of your type, you should be able to implement the count trees and
the index trees as instances.
16.8 Sets
A finite set is a collection of elements of a particular type, which is both like and
unlike a list. Lists are, of course, familiar, and examples include
[Joe,Sue,Ben] [Ben,Sue,Joe]
[Joe,Sue,Sue,Ben] [Joe,Sue,Ben,Sue]
Each of these lists is different – not only do the elements of a list matter, but also the
order in which they occur and the number of times that each element occurs (its
multiplicity) are significant.
In many situations, order and multiplicity are irrelevant. If we want to talk about
the collection of people going to a birthday party, we just want the names; a person
is either there or not and so multiplicity is not important and the order in which we
might list them is also of no interest. In other words, all we want to know is the set of
people coming. In the example above, this is the set consisting of Joe, Sue and Ben.
Like lists, queues, trees and so on, sets can be combined in many different ways:
the operations which combine sets form the signature of the abstract data type. The
search trees we saw earlier provide operations which concentrate on elements of a
single ordered set: ‘what is the successor of element e in set s?’ for instance.
In this section we focus on the combining operations for sets. The signature for
sets is as follows. We explain the purpose of the operations at the same time as giving
their implementation.
422 CHAPTER 16. ABSTRACT DATA TYPES
module Set
( Set ,
empty , -- Set a
sing , -- a -> Set a
memSet , -- Ord a => Set a -> a -> Bool
union,inter,diff , -- Ord a => Set a -> Set a -> Set a
eqSet , -- Eq a => Set a -> Set a -> Bool
subSet , -- Ord a => Set a -> Set a -> Bool
makeSet , -- Ord a => [a] -> Set a
mapSet , -- Ord b => (a -> b) -> Set a -> Set b
filterSet , -- (a->Bool) -> Set a -> Set a
foldSet , -- (a -> a -> a) -> a -> Set a -> a
showSet , -- (a -> String) -> Set a -> String
card -- Set a -> Int
) where
There are numerous possible signatures for sets, some of which assume certain prop-
erties of the element type. To test for elementhood, we need the elements to belong
to a type in the Eq class; here we assume that the elements are in fact from an ordered
type, which enlarges the class of operations over Set a. This gives the contexts Ord
a and Ord b, which are seen in some of the types in the signature above.
The principal definitions over Set a are given in Figures 16.6 and 16.7. At the start
of the file we see that we import the library List, but as there is a definition of union
in there we have to hide this on import, thus,
Also at the start of the file we give the instance declarations for the type. It is im-
portant to list these at the start because there is no explicit record of them in the
module header.
We now run through the individual functions as they are implemented in Figures
16.6 and 16.7. In our descriptions we use curly brackets ‘{’, ‘}’, to represent sets in
examples – this is emphatically not part of Haskell notation.
Empty set and singleton. The empty set {} is represented by an empty list, and
the singleton set {x}, consisting of the single element x, by a one-element list.
empty :: Set a
empty = Set []
Figure 16.6: Operations over the set abstract data type, part 1.
424 CHAPTER 16. ABSTRACT DATA TYPES
Figure 16.7: Operations over the set abstract data type, part 2.
16.8. SETS 425
than the element y which we seek, and so we should check recursively for the pres-
ence of y in the tail xs. In case (memSet.2) we have found the element, while in
case (memSet.3) the head element is larger than y; since the list is ordered, all el-
ements will be larger than y, so it cannot be a member of the list. This definition
would not work if we chose to use arbitrary lists to represent sets.
In making these definitions we again exploit the fact that the two arguments are
ordered. We also define the functions by ‘wrapping up’ a function over the ‘bare’ list
type. For instance, in defining union we first define
which works directly over ordered lists, and then make a version which works over
Set,
Recall that the brackets ‘{’, ‘}’ are not a part of Haskell; we can see them as shorthand
for Haskell expressions as follows.
Subset and equality tests. To test whether the first argument is a subset of the sec-
ond, we use subSet; x is a subset of y if every element of x is an element of y.
Two sets are going to be equal if their representations as ordered lists are the
same – hence the definition of eqSet as list equality; note that we require equality
on a to define equality on Set a. The function eqSet is exported as part of the
signature, but also we declare an instance of the Eq class, binding == to eqSet thus
The ADT equality will not in general be the equality on the underlying type: if we
were to choose arbitrary lists to model sets, the equality test would be more complex,
since [1,2] and [2,1,2,2] would represent the same set.
426 CHAPTER 16. ABSTRACT DATA TYPES
The subset ordering is not bound to <= since it is customary for the <= in Ord to be a
total order, that is for all elements x and y, either x<=y or y<=x will hold. The subset
ordering is not a total order, while the lexicographic ordering over (ordered) lists is
total. Some examples for comparison are given in the exercises.
Construction. To form a set from an arbitrary list, makeSet, the list is sorted, and
then duplicate elements are removed, before it is wrapped with Set. The definition
of sort is imported from the List library.
Higher-order functions. mapSet, filterSet and foldSet behave like map, filter
and foldr except that they operate over sets. The latter two are essentially given by
filter and foldr; in mapSet duplicates have to be removed after mapping.
Print. showSet f (Set xs) gives a printable version of a set, one item per line,
using the function f to give a printable version of each element.
Cardinality. The cardinality of a set is the number of its members. The function
card gives this, as it returns the length of the list.
In the next section we build a library of functions to work with relations and graphs
which uses the Set library as its basis.
Exercises
16.35 Compare how the following pairs of sets are related by the orderings <= and
subSet.
{3} {3,4}
{2,3} {3,4}
{2,9} {2,7,9}
16.36 Define the function diff so that diff s1 s2 consists of the elements of s1
which do not belong to s2.
which gives the symmetric difference of two sets. This consists of the ele-
ments which lie in one of the sets but not the other, so that
16.9. RELATIONS AND GRAPHS 427
which returns the set of all subsets of a set defined? Can you give a definition
which uses only the operations of the abstract data type and not the concrete
implementation?
which return the union and intersection of a set of sets defined using the op-
erations of the abstract data type?
16.40 Can infinite sets (of numbers, for instance) be adequately represented by or-
dered lists? Can you tell if two infinite lists are equal, for instance?
16.41 The abstract data type Set a can be represented in a number of different
ways. Alternatives include arbitrary lists (rather than ordered lists without
repetitions) and Boolean valued functions, that is elements of the type a ->
Bool. Give implementations of the type using these two representations.
16.42 Give an implementation of the Set abstract data type using search trees.
16.43 Give an implementation of the search tree abstract data type using ordered
lists. Compare the behaviour of the two implementations.
16.44 Give a set of QuickCheck properties for the Set type. These should reflect
the mathematical properties of sets. Hint: you could begin by thinking about
corresponding properties of lists, and about which of these you would expect
to hold for sets and which would not.
Relations
A binary relation relates together certain elements of a set. A family relationship
can be summarized by saying that the isParent relation holds between Ben and
Sue, between Ben and Leo and between Sue and Joe. In other words, it relates the
pairs (Ben,Sue), (Ben,Leo) and (Sue,Joe), and so we can think of this particular
relation as the set
isParent = {(Ben,Sue) , (Ben,Leo) , (Sue,Joe)}
In general we say
type Relation a = Set (a,a)
This definition means that all the set operations are available on relations. We can
test whether a relation holds of two elements using memSet; the union of two rela-
tions like isParent and isSibling gives the relationship of being either a parent
or a sibling, and so on.
We look at two examples of family relations, based on a relation isParent which
we assume is given to us. We first set ourselves the task of defining the function
addChildren which adds to a set of people all their children; we then aim to define
the isAncestor relation. The full code for the functions discussed here is given in
Figure 16.8.
Defining addChildren
The image of an element. Working bottom-up, we first ask how we find all ele-
ments related to a given element: who are all Ben’s children, for instance? We need
to find all pairs beginning with Ben, and then return their second halves. The func-
tion to perform this is called image and the set of Ben’s children will be
image isParent Ben = {Sue,Leo}
The image of a set of elements. Now, how can we find all the elements related to
a set of elements? We find the image of each element separately and then take the
union of these sets. The union of a set of sets is given by folding the binary union
operation into the set.
unionSet {s1 , ... ,sn }
= s1 [ ... [ sn
= s1 ‘union‘ ... ‘union‘ sn
Now, how do we add all the children to a set of people? We find the image of the
set under isParent, and combine it with the set itself. This is given by the function
addChildren.
Defining isAncestor
The second task we set ourselves was to find the isAncestor relation. The gen-
eral problem is to find the transitive closure of a relation, the function tClosure
16.9. RELATIONS AND GRAPHS 429
setProduct :: (Ord a,Ord b) => Set a -> Set b -> Set (a,b)
setProduct st1 st2 = unionSet (mapSet (adjoin st1) st2)
(Ben,Sue) (Sue,Joe)
and see that this gives that Ben is a grandparent of Joe. We call this the relational
composition of isParent with itself. In general,
isGrandparent
= isParent ‘compose‘ isParent
= {(Ben,Joe)}
Set product. In defining compose we have used the setProduct function to give
the product of two sets. This is formed by pairing every element of the first set with
every element of the second. For instance,
setProduct uses the function adjoin to pair each element of a set with a given
element. For instance,
Transitive closure and limits. A relation rel is transitive if for all (a,b) and (b,c)
in rel, (a,c) is in rel. The transitive closure of a relation rel is the smallest rela-
tion extending rel which is transitive. We compute the transitive closure of rel,
tClosure rel, by repeatedly adding one more ‘generation’ of rel, using compose,
until nothing more is added.
To do this, we make use of the limit function, a polymorphic higher-order func-
tion of general use. limit f x gives the limit of the sequence
x , f x , f (f x) , f (f (f x)) , ...
The limit is the value to which the sequence settles down if it exists. It is found by
taking the first element in the sequence whose successor is equal to the element
itself.
Example. As an example, take Ben to be Sue’s father, Sue to be Joe’s mother, who
himself has no children. Now define
to add to a set the children of all members of the set, so that for instance
16.9. RELATIONS AND GRAPHS 431
Context simplification
The functions of Figure 16.8 give an interesting example of context simplification for
type classes. The adjoin function requires that the types a and b carry an ordering.
Haskell contains the instance declaration
Graphs
Another way of seeing a relation is as a directed graph. For example, the relation
1 2
3 4
where we draw an arrow joining a to b if the pair (a,b) is in the relation. What then
does the transitive closure represent? Two points a and b are related by tClosure
graph1 if there is a path from a to b through the graph. For example, the pair (1,4)
is in the closure, since a path leads from 1 to 3 then to 2 and finally to 4, while the
pair (2,1) is not in the closure, since no path leads from 2 to 1 through the graph.
432 CHAPTER 16. ABSTRACT DATA TYPES
• we first form the relation which links points in the same component, then
• we form the components (or equivalence classes) generated by this relation.
There is a path from x to y and vice versa if both (x,y) and (y,x) are in the closure,
so we define
Now, how do we form the components given by the relation graph1? We start with
the set
{{1},{2},{3},{4}}
and repeatedly add the images under the relation to each of the classes, until a fixed
point is reached. In general this gives
addImages :: Ord a => Relation a -> Set (Set a) -> Set (Set a)
addImages rel = mapSet (addImage rel)
16.9. RELATIONS AND GRAPHS 433
Searching in graphs
Many algorithms require us to search through the nodes of a graph: we might want
to find a shortest path from one point to another, or to count the number of paths
between two points.
Two general patterns of search are depth-first and breadth-first. In a depth-first
search, we explore all elements below a given child before moving to the next child;
a breadth-first search examines all the children before examining the grandchil-
dren, and so on. In the case of searching below node 1 in graph1, the sequence
[1,2,4,3] is depth-first (4 is visited before 3), while [1,2,3,4] is breadth-first.
These examples show that we can characterize the searches as functions
with breadthFirst graph1 1 = [1,2,3,4], for instance. The use of a list in these
functions is crucial – we are not simply interested in finding the nodes below a node
(tClosure does this), we are interested in the order in which they occur.
The essential step in both searches is to find all the descendants of a node which
have not been visited so far. We can write
which returns the set of descendants of v in rel which are not in the set st. Here
we have a problem; the result of this function is a set and not a list, but we require
the elements in some order. One solution is to add to the Set abstract data type a
function
which breaks the abstraction barrier in the case of the ordered list implementation.
An alternative is to supply as a parameter a function
which returns the minimum of a non-empty set and which can be used in flattening
a set to a list without breaking the abstraction barrier. Unconcerned about its par-
ticular definition, we assume the existence of a flatten function of type (setList).
Then we can say
Breadth-first search
• First, all the descendants of elements in xs which are not already in xs are
found. This is given by mapping (findDescs rel xs) along the list xs.
• This list of lists is then concatenated into a single list.
• Duplicates can occur in this list, as a node may be a descendant of more than
one node, and so any duplicated elements must be removed. This is the effect
of the library function
which removes all but the first occurrence of each element in a list.
Depth-first search
depthList :: Ord a => Relation a -> [a] -> [a] -> [a]
The definition has two equations, the first giving the trivial case where no nodes are
to be explored. In the second there are two parts to the solution:
• next gives the part of the graph accessible below val. This may be [], if val
is a member of the list used, otherwise depthSearch is called.
• depthList is then called on the tail of the list, but with next appended to the
list of nodes already visited.
Exercises
16.45 Calculate
breadthFirst graph2 1
depthFirst graph2 1
which gives the length of a shortest path from one node to another in a graph.
For instance,
distance graph1 1 4 = 2
distance graph1 4 1 = 0
0 is the result when no such path exists, or when the two nodes are equal.
16.48 A weighted graph carries a numerical weight with each edge. Design a type
to model this. Give functions for breadth-first and depth-first search which
return lists of pairs. Each pair consists of a node, together with the length of a
shortest path to that node from the node at the start of the search.
16.50 [Harder] Formulate QuickCheck properties for the search functions given in
this section. You should try to think of properties which are shared by all
search functions, and also other properties which hold of particular search
functions.
16.10 Commentary
This section explores a number of issues raised by the introduction of ADTs into our
repertoire.
First, we have not yet said anything about verification of functions over abstract
data types. This is because there is nothing new to say about the proof of theorems:
these are proved for the implementation types exactly as we have seen earlier. The
theorems valid for an abstract data type are precisely those which obey the type con-
straints on the functions in the signature. For a queue type, for instance, we will be
able to prove that
by proving the appropriate result about the implementation. What would not be
valid would be an equation like
emptyQ = Qu []
since this breaks the information-hiding barrier and reveals something of the im-
plementation itself.
Next we note that our implementation of sets gives rise to some properties which
we ought to prove, often called invariants. We have assumed that our sets are imple-
mented as ordered lists without repetitions; we ought to prove that each operation
over our implementation preserves this property. Both the properties of the func-
tions, and the invariants over the implementation, can be formulated as QuickCheck
properties; we leave these as exercises for the reader.
Finally, observe that both classes and abstract data types use signatures, so it is
worth surveying their similarities and differences.
• Their purposes are different: ADTs are used to provide information hiding,
and to structure programs; classes are used to overload names, to allow the
same name to be used over a class of different types.
• The functions in the signature of an ADT provide the only access to the under-
lying type. There is no such information hiding over classes: to be a member
of a class, a type must provide at least the types in signature.
16.10. COMMENTARY 437
Summary
The abstract data types of this chapter have three important and related properties.
We saw various examples of ADT development. Most importantly we saw the prac-
tical example of the simulation types being designed in the three stages suggested.
First the types are named, then they are described informally and finally a signature
is written down. After that we are able to implement the operations of the signature
as a separate task.
One of the difficulties in writing a signature is being sure that all the relevant
operations have been included; we have given a check-list of the kinds of operations
which should be present, and against which it is sensible to evaluate any candidate
signature definitions.
438 CHAPTER 16. ABSTRACT DATA TYPES
Chapter 17
Lazy programming
In our calculations so far we have said that the order in which we make evaluation
steps will not affect the results produced – it may only affect whether the sequence
leads to a result. This chapter describes precisely the lazy evaluation strategy which
underlies Haskell. Lazy evaluation is well named: a lazy evaluator will only evaluate
an argument to a function if that argument’s value is needed to compute the overall
result. Moreover, if an argument is structured (a list or a tuple, for instance), only
those parts of the argument which are needed to make the computation continue
will be evaluated; we’ll look at how this works in practice in some examples.
Lazy evaluation has consequences for the style of programs we can write. Since
an intermediate list will only be generated on demand, using an intermediate list
will not necessarily be expensive computationally. We examine this in the context of
a series of examples, culminating in a case study of parsing.
To build parsers we construct a toolkit of polymorphic, higher-order functions
which can be combined in a flexible and extensible way to make language proces-
sors of all sorts. One of the distinctive features of a functional language is the col-
lection of facilities it provides for defining libraries like this; we’ll come back to this
example when we discuss domain-specific languages in Chapter 19.
We also take the opportunity to extend the list comprehension notation. This
does not allow us to write any new programs, but does make a lot of list process-
ing programs – especially those which work by generating and then testing possible
solutions – easier to express and understand.
Another consequence of lazy evaluation is that it is possible for the language
to describe infinite structures. These would require an infinite amount of time to
evaluate fully, but under lazy evaluation, only parts of a data structure need to be
examined. Any recursive type will contain infinite objects; we concentrate on lists
here, as infinite lists are by far the most widely used infinite structures.
After introducing a variety of examples, such as infinite lists of prime and ran-
dom numbers, we discuss the importance of infinite lists for program design, and see
that programs manipulating infinite lists can be thought of as processes consuming
and creating ‘streams’ of data. Based on this idea, we explore how to complete the
simulation case study.
439
440 CHAPTER 17. LAZY PROGRAMMING
The chapter concludes with an update on program verification in the light of lazy
evaluation and the existence of infinite lists; this section can only give a flavour of
the area, but contains references to more detailed presentations.
Sections 17.1 and 17.2 are essential reading, but the sections that follow explore
the impact of laziness on a number of things we have looked at already, including
list comprehensions and the simulation case study, as well as examining design and
verification questions.
f x y = x+y
then
f (9-3) (f 34 3)
; (9-3)+(f 34 3)
since we replace x by (9-3) and y by (f 34 3). The expressions (f 34 3) and
(9-3) are not evaluated before they are passed to the function.
In this case, for evaluation to continue, we need to evaluate the arguments to ‘+’,
giving
; 6+(f 34 3)
; 6+(34+3)
; 6+37
; 43
In this example, both of the arguments are evaluated eventually, but this is not al-
ways the case. If we define
g x y = x+12
then
g (9-3) (g 34 3)
; (9-3)+12
; 6+12
; 18
Here (9-3) is substituted for x, but as y does not appear on the right-hand side of
the equation, the argument (g 34 3) will not appear in the result, and so is not
evaluated. Here we see the first advantage of lazy evaluation: an argument which
is not needed will not be evaluated. This example is rather too simple: why would
we write the second argument if its value is never needed? A rather more realistic
example is
17.1. LAZY EVALUATION 441
If the integer n is positive, the result is the value of x; otherwise it is the value of y.
Either of the arguments x and y might be used, but in the first case y is not evaluated
and in the second x is not evaluated. A third example is
h x y = x+x
so that
h (9-3) (h 34 3) (h-eval)
; (9-3)+(9-3)
It appears here that we will have to evaluate the argument (9-3) twice since it is
duplicated on substitution. Lazy evaluation ensures that a duplicated argument is
never evaluated more than once. This can be modelled in a calculation by doing the
corresponding steps simultaneously, thus
h (9-3) 17
; (9-3)+(9-3)
; 6+6
; 12
(a) + (b) +
pm (x,y) = x+1
pm (3+2,4-17)
; (3+2)+1
; 6
The argument is examined, and part of it is evaluated. The second half of the pair
remains unevaluated, as it is not needed in the calculation. This completes the infor-
mal introduction to lazy evaluation, which can be summarized in the three points:
442 CHAPTER 17. LAZY PROGRAMMING
• arguments to functions are evaluated only when this is necessary for evalua-
tion to continue;
• an argument is not necessarily evaluated fully: only the parts that are needed
are examined;
• an argument is evaluated at most only once. This is done in the implementa-
tion by replacing expressions by graphs and calculating over them.
We now give a more formal account of the calculation rules which embody lazy eval-
uation.
f p1 p2 ... pk
| g1 = e1
| g2 = e2
...
| otherwise = er
where
v1 a1,1 ... = r1
....
f q1 q2 ... qk
= ...
In calculating f a1 ... ak there are three aspects.
f [1 .. 3] [1 .. 3] (1)
; f (1:[2 .. 3]) [1 .. 3] (2)
; f (1:[2 .. 3]) (1:[2 .. 3]) (3)
; 1+1 (4)
At stage (1), there is not enough information about the arguments to determine
whether there is a match with (f.1). One step of evaluation gives (2), and shows
there is not a match with (f.1).
The first argument of (2) matches the first pattern of (f.2), so we need to check
the second. One step of calculation in (3) shows that there is no match with (f.2),
but that there is with (f.3); hence we have (4).
Calculation – guards
Suppose that the first conditional equation matches (simply for the sake of explana-
tion). The expressions a1 to ak are substituted for the patterns p1 to pk throughout
the conditional equation. We must next determine which of the clauses on the right-
hand side applies. The guards are evaluated in turn, until one is found which gives
the value True; the corresponding clause is then used. If we have
f m n
| notNil xs = front xs
| otherwise = n
where
xs = [m .. n]
notNil [] = False
notNil (_:_) = True
the calculation of f 3 5 will be
f 3 5
?? notNil xs
?? where
?? xs = [3 .. 5]
?? ; 3:[4 .. 5] (1)
?? ; notNil (3:[4 .. 5])
?? ; True
; front xs
where
xs = 3:[4 .. 5]
; 3:4:[5] (2)
; 3+4 (3)
; 7
To evaluate the guard notNil xs, evaluation of xs begins, and after one step, (1)
shows that the guard is True. Evaluating front xs requires more information about
xs, and so we evaluate by one more step to give (2). A successful pattern match in
the definition of front then gives (3), and so the result.
This section has described where clauses; a similar explanation applies to let
expressions.
True && x = x
False && x = False
then they will follow the rules for Haskell definitions. The left-to-right order means
that ‘&&’ will not evaluate its second argument in the case that its first is False, for
17.3. LIST COMPREHENSIONS REVISITED 445
instance. This is unlike many programming languages, where the ‘and’ function will
evaluate both its arguments.
The other operations, such as the arithmetic operators, vary. Multiplication needs
both its arguments to return a result1 but the equality on lists can return False on
comparing [] and (x:xs) without evaluating x or xs. In general the language is
implemented so that no manifestly unnecessary evaluation takes place.
Recall that if . . . then . . . else . . . ; cases; let and lambda expressions can be
used in forming expressions. Their evaluation follows the form we have seen for
function applications. Specifically, if . . . then . . . else . . . is evaluated like a guard,
cases like a pattern match, let like a where clause and a lambda expression like
the application of a named function such as f above.
Finally, we turn to the way in which a choice is made between applications.
Evaluation order
What characterizes evaluation in Haskell, apart from the fact that no argument is
evaluated more than once, is the order in which applications are evaluated when
there is a choice.
f1 e1 (f2 e2 17)
where one application encloses another, the outer one is evaluated. In the
example, the outer one, f1 e1 (f2 e2 17), is chosen for evaluation.
• Otherwise, evaluation is from left to right. In the expression
f1 e1 + f2 e2
These rules are enough to describe the way in which lazy evaluation works. In the
sections to come we look at the consequences of a lazy approach for functional pro-
gramming.
are generated and combinations of them are tested, before results depending upon
them are returned.
We begin the section with a re-examination of the syntax of the list comprehen-
sion, before giving some simple examples to illustrate the features that we describe.
After that we give the rules for calculating with list comprehensions, and we finish
the section with a series of longer examples.
Syntax
A list comprehension has the form
[ e | q1 , ... , qk ]
An expression lExp or bExp appearing in qualifier qi can refer to the variables used
in the patterns of qualifiers q1 to qi-1 .
Simpler examples
Multiple generators allow us to combine elements from two or more lists
This example is important as it shows the way in which the values x and y are cho-
sen.
The first element of xs, 1, is given to x, and then for this fixed value all possible
values of y in ys are chosen. This process is repeated for the remaining values x in
xs, namely 2 and 3.
This choice is not accidental, since if we have
the second generator, y <- [1 .. x] depends on the value of x given by the first
generator.
triangle 3
; [(1,1),(2,1),(2,2),(3,1),(3,2),(3,3)]
17.3. LIST COMPREHENSIONS REVISITED 447
For the first choice of x, 1, the value of y is chosen from [1 .. 1], for the second
choice of x, the value of y is chosen from [1 .. 2], and so on.
Three positive integers form a Pythagorean triple if the sum of squares of the
first two is equal to the square of the third. The list of all triples with all sides below
a particular bound, n, is given by
pyTriple n
= [ (x,y,z) | x <- [2 .. n] , y <- [x+1 .. n] ,
z <- [y+1 .. n] , x*x + y*y == z*z ]
pyTriple 100
; [(3,4,5),(5,12,13),(6,8,10),...,(65,72,97)]
where the values 1 and 2 are substituted for x. The rules for tests are simple,
[ e | True , q2 , ... , qk ]
; [ e | q2 , ... , qk ]
[ e | False , q2 , ... , qk ]
; []
[ e | ] = [ e ]
triangle 3
; [ (x,y) | x <- [1 .. 3] , y <- [1 .. x] ]
; [ (1,y) | y <- [1 .. 1] ] ++
[ (2,y) | y <- [1 .. 2] ] ++
[ (3,y) | y <- [1 .. 3] ]
; [ (1,1) | ] ++
[ (2,1) | ] ++ [ (2,2) | ] ++
[ (3,1) | ] ++ [ (3,2) | ] ++ [ (3,3) | ]
; [(1,1),(2,1),(2,2),(3,1),(3,2),(3,3)]
We now look at two longer examples, the solutions for which are aided by the list
comprehension style.
17.3. LIST COMPREHENSIONS REVISITED 449
Example
List permutations
A permutation of a list is a list with the same elements in a different order. For a list
of n elements, there are n! (n factorial) permutations of the list. The perms function
returns a list of all permutations of a list.
perms [] = [[]]
perms xs = [ x:ps | x <- xs , ps <- perms (xs\\[x]) ]
Example evaluations give, for a one-element list,
perms [2]
; [x:ps| x <- [2] , ps <- perms [] ]
; [x:ps| x <- [2] , ps <- [[]] ]
; [2:ps| ps <- [[]] ]
; [2:[] | ]
; [[2]]
for a two-element list,
perms [2,3]
; [ x:ps | x <- [2,3] , ps <- perms([2,3]\\[x]) ]
; [ 2:ps | ps <- perms [3] ] ++ [ 3:ps | ps <- perms [2] ]
; [ 2:[3] ] ++ [ 3:[2] ]
; [ [2,3] , [3,2] ]
and finally for a three-element list,
perms [1,2,3]
; [ x:ps | x <- [1,2,3] , ps <- perms([1,2,3]\\[x]) ]
; [ 1:ps | ps <- perms [2,3]] ++...++ [ 3:ps | ps <- perms [1,2]]
; [ 1:ps | ps<-[[2,3],[3,2]]] ++...++ [ 3:ps | ps<-[[1,2],[2,1]]]
; [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
There is another algorithm for permutations: in this, a permutation of a list (x:xs)
is given by forming a permutation of xs, and by inserting x into this somewhere. The
possible insertion points are given by finding all the possible splits of the list into two
halves.
450 CHAPTER 17. LAZY PROGRAMMING
perm [] = [[]]
perm (x:xs) = [ ps++[x]++qs | rs <- perm xs ,
(ps,qs) <- splits rs ]
We get the list of all possible splits of a list xs after seeing that on splitting (y:ys),
we either split at the front of (y:ys), or somewhere inside ys, as given by a split of
ys.
splits :: [a]->[([a],[a])]
splits [] = [ ([],[]) ]
splits (y:ys) = ([],y:ys) : [ (y:ps,qs) | (ps,qs) <- splits ys]
Before moving on, observe that the type of perms requires that a must be in the class
Eq. This is needed for the list difference operator \\ to be defined over the type [a].
There is no such restriction on the type of perm, which uses a different method for
calculating the permutations.
Exercises
which return all the sublists and subsequences of a list. A sublist is obtained by
omitting some of the elements of a list; a subsequence is a continuous block
from a list. For instance, both [2,4] and [3,4] are sublists of [2,3,4], but
only [3,4] is a subsequence.
perm [2]
perm [2,3]
perm [1,2,3]
matrixProduct [[2.0,3.0,4.0],[5.0,6.0,-1.0]]
[[1.0,0.0],[1.0,1.0],[0.0,-1.0]]
17.5 Define functions to calculate the determinant of a square matrix and, if this is
non-zero, to invert the matrix.
17.6 The calculation rules for list comprehensions can be re-stated for the two cases
[] and (x:xs), instead of for the arbitrary list [a1 ,...,an ]. Give these rules
by completing the equations
17.7 Give the precise rules for calculating with a generator containing a refutable
pattern, like (x:xs) <- lExp. You might need to define auxiliary functions
to do this.
17.8 List comprehensions can be translated into expressions involving map, filter
and concat by the following equations.
[ x | x<-xs ] = xs
[ f x | x<-xs ] = map f xs
[ e | x<-xs , p x , ... ] = [ e | x <- filter p xs , ... ]
[ e | x<-xs , y<-ys , .. ] = concat [ [e|y<-ys, ..] | x<-xs]
17.4. DATA-DIRECTED PROGRAMMING 453
using these equations; you will need to define some auxiliary functions as a
part of your translation.
As a program, we have
sumFourthPowers n
; sum (map (ˆ4) [1 .. n])
create first part of the list with :
; sum (map (ˆ4) (1:[2 .. n])) allows pattern match
; sum (1ˆ4 : map (ˆ4) [2 .. n]) by definition of map
; (1ˆ4) + sum (map (ˆ4) [2 .. n]) by definition of sum
now we can recycle the first part of the list
; 1 + sum (map (ˆ4) [2 .. n])
; ...
; 1 + (16 + sum (map (ˆ4) [3 .. n]))
; ...
; 1 + (16 + (81 + ... + n4 ))
454 CHAPTER 17. LAZY PROGRAMMING
As can be seen, none of the intermediate lists is created in full in this calculation. As
soon as the first part of the list is created, its fourth power is taken, and it becomes a
part of the sum which produces the final result, and so the first part can be recycled.
Example
1. List minimum
A more striking example is given by the problem of finding the minimum of a list of
numbers. One solution is to sort the list, and take its head! This would be ridiculous
if the whole list were sorted in the process, but, in fact we have, using the definition
of insertion sort from Chapter 7,
iSort [8,6,1,7,5]
; ins 8 (ins 6 (ins 1 (ins 7 (ins 5 []))))
; ins 8 (ins 6 (ins 1 (ins 7 [5])))
; ins 8 (ins 6 (ins 1 (5 : ins 7 [])))
; ins 8 (ins 6 (1 : (5 : ins 7 [])))
; ins 8 (1 : ins 6 (5 : ins 7 []))
; 1 : ins 8 (ins 6 (5 : ins 7 []))
As can be seen from the underlined parts of the calculation, each application of ins
calculates the minimum of a larger part of the list, since the head of the result of ins
is given in a single step. The head of the whole list is determined in this case without
us working out the value of the tail, and this means that we have a sensible algorithm
for minimum given by (head . iSort).
1 3 5
2 4 6
instead of returning one result, or an error if there is none, we return a list; the error
case is signalled by the empty list. The method also allows for multiple results to be
returned, as we shall see.
How do we solve the new problem?
Acyclic case. For the present we assume that the graph is acyclic: there is no circu-
lar path from any node back to itself.
We therefore look for all paths from x to y going through z, for each neighbour z of
x.
where flatten turns a set into a list. Now consider the example, where we write
routes’ for routes graphEx and nbhrs’ for nbhrs graphEx, to make the calcu-
lation more readable:
routes’ 1 4
; [ 1:r | z <- nbhrs’ 1 , r <- routes’ z 4 ]
; [ 1:r | z <- [2,3] , r <- routes’ z 4 ]
; [ 1:r | r <- routes’ 2 4 ] ++
[ 1:r | r <- routes’ 3 4 ] (†)
; [ 1:r | r <- [ 2:s | w <- nbhrs’ 2 , s <- routes’ w 4 ]]++...
; [ 1:r | r <- [ 2:s | w <- [4] , s <- routes’ w 4 ] ] ++ ...
; [ 1:r | r <- [ 2:s | s <- routes’ 4 4 ] ] ++ ... (‡)
; [ 1:r | r <- [ 2:s | s <- [[4]] ] ] ++ ...
; [ 1:r | r <- [ [2,4] ] ] ++ ...
; [[1,2,4]] ++ ...
The head of the list is given by exploring only the first neighbour of 1, namely 2, and
its first neighbour, 4. In this case the search for a route leads directly to a result. This
is not always so. Take the example of
routes’ 1 6 = ...
; [ 1:r | r <- routes’ 2 6 ] ++
456 CHAPTER 17. LAZY PROGRAMMING
General case. We assumed at the start of this development that the graph was
acyclic, so that we have no chance of a path looping back on itself, and so of a search
going into a loop. We can make a simple addition to the program to make sure that
only paths without cycles are explored, and so that the program will work for an ar-
bitrary graph. We add a list argument for the points not to be visited (again), and so
have
routesC :: Ord a => Relation a -> a -> a -> [a] -> [[a]]
routesC rel x y avoid
| x==y = [[x]]
| otherwise = [ x:r | z <- nbhrs rel x \\ avoid ,
r <- routesC rel z y (x:avoid) ]
Two changes are made in the recursive case.
• In looking for neighbours of x we look only for those which are not in the list
avoid;
• in looking for routes from z to y, we exclude visiting both the elements of
avoid and the node x itself.
A search for a route from x to y in rel is given by routesC rel x y [].
Exercises
makeSet [(1,2),(2,1),(1,3),(2,4),(3,5),(5,6),(3,6)]
routes graphEx 1 4
routes rel x y
| x==y = [[x]]
| otherwise = [ x:r | z <- nbhrs rel x ,
r <- routes rel z y ,
not (elem x r) ]
and explain why this definition is not suitable for use on cyclic graphs. Finally,
give a calculation of
routesC graphEx 1 4 []
The first is a parser which always fails, so accepts nothing. There are no entries
in its output list.
none :: Parse a b
none inp = []
On the other hand, we can succeed immediately, without reading any input. The
value recognized is a parameter of the function.
Combining parsers
Here we build a library of higher-order polymorphic functions, which we then use
to give our parser for expressions. First we have to think about the ways in which
parsers need to be combined.
Looking at the expression example, an expression is either a literal, or a variable
or an operator expression. From parsers for the three sorts of expression, we want
to build a single parser for expressions. For this we use alt
460 CHAPTER 17. LAZY PROGRAMMING
infixr 5 >*>
none :: Parse a b
none inp = []
infixr 5 >*>
(>*>) p1 p2 inp
= [((y,z),rem2) | (y,rem1) <- p1 inp , (z,rem2) <- p2 rem1 ]
The values (y,rem1) run through the possible results of parsing inp using p1. For
each of these, we apply p2 to rem1, which is the input which is unconsumed by p1
in that particular case. The results of the two successful parses, y and z, are returned
as a pair.
As an example, assume that digList recognizes non-empty sequences of dig-
its, and look at (digList >*> bracket) "24(". Applying digList to the string
"24(" gives two results,
digList "24(" ; [("2","4(") , ("24","(")]
and so (y,rem1) runs through two cases
• A list can be empty, which will be recognized by the parser succeed [].
• Any other list is non-empty, and consists of an object followed by a list of ob-
jects. A pair like this is recognized by p >*> list p; we then have to turn
this pair (x,xs) into the list (x:xs), for which we use build, applied to the
uncurried form of (:), which takes its arguments as a pair, and thus converts
(x,xs) to (x:xs).
Exercises
so that neList p recognizes a non-empty list of the objects which are rec-
ognized by p, and optional p recognizes such an object optionally – it may
recognize an object or succeed immediately.
opExpParse
= (token ’(’ >*>
parser >*>
spot isOp >*>
parser >*>
token ’)’)
‘build‘ makeExpr
where the conversion function takes a nested sequence of pairs, like
litParse
= ((optional (token ’˜’)) >*>
(neList (spot isDigit))
‘build‘ (charlistToExpr . uncurry (++))
Left undefined here is the function charlistToExpr which should convert a list of
characters to a literal integer; this is an exercise for the reader.
Exercises
Explain how you could define numberRec from number, and then give a gen-
eral set of library functions for building recognisers in a similar way to the
library for building parsers.
so that
17.15 A command to the calculator to assign the value of expr to the variable var is
represented thus
var:expr
17.16 How would you change the parser for numbers if decimal fractions are to be
allowed in addition to integers?
17.17 How would you change the parser for variables if names longer than a single
character are to be allowed?
17.18 Explain how you would modify your parser so that the whitespace characters
space and tab can be used in expressions, but would be ignored on parsing.
(Hint: there is a simple pre-processor which does the trick!)
17.19 (Note: this exercise is for those familiar with Backus-Naur notation for gram-
mars.)
Expressions without bracketing and allowing the multiplicative expressions
higher binding power are described by the grammar
Give a Haskell parser for this grammar. Discuss the associativity of the opera-
tor ‘-’ in this grammar.
The parse p inp is successful if the result contains at least one parse (the second
case) in which all the input has been read (the test given by the pattern match to
(found,[])). If this happens, the first value found is returned; otherwise (the first
case) we return the default value of type b, defaultVal, which in our case study will
be the default command, Null.
We can define the type of commands thus
If the assignment command takes the form var:expr, then it is not difficult to de-
sign a parser for this type,
Conclusions
The type of parsers Parse a b with the functions
none :: Parse a b
succeed :: b -> Parse a b
spot :: (a -> Bool) -> Parse a a
alt :: Parse a b -> Parse a b -> Parse a b
>*> :: Parse a b -> Parse a c -> Parse a (b,c)
build :: Parse a b -> (b -> c) -> Parse a c
topLevel :: Parse a b -> [a] -> b
allow us to construct so-called recursive descent parsers in a straightforward way. It
is worth looking at the aspects of the language we have exploited.
• The type Parse a b is represented by a function type, so that all the parser
combinators are higher order functions.
• Because of polymorphism, we do not need to be specific about either the in-
put or the output type of the parsers we build.
In our example we have confined ourselves to inputs which are strings of
17.5. CASE STUDY: PARSING EXPRESSIONS 467
characters, but they could have been tokens of any other type, if required: we
might take the tokens to be words which are then parsed into sentences, for
instance.
More importantly in our example, we can return objects of any type using
the same combinators, and in the example we returned lists and pairs as well
as simple characters and expressions.
• Lazy evaluation plays a role here also. The possible parses we build are gen-
erated on demand as the alternatives are tested. The parsers will backtrack
through the different options until a successful one is found.
Building general libraries like this parser library is one of the major advantages of us-
ing a modern functional programming language with the facilities mentioned above.
From a toolkit like this it is possible to build a whole range of parsers and language
processors which can form the front ends of systems of all sorts.
We will return to a discussion of parsing in Chapter 18; note also that we could
make the type of Parse a b into an abstract data type, along the lines discussed in
Chapter 16. On the other hand, it would also be useful to leave the implementation
open to extension by users, which is the way in which other Haskell libraries are
made available.
Exercises
17.20 Define a parser which recognizes strings representing Haskell lists of integers,
like "[2,-3,45]".
17.21 Define a parser to recognize simple sentences of English, with a subject, verb
and object. You will need to provide some vocabulary, "cat", "dog", and so
on, and a parser to recognize a string. You will also need to define a function
whose parameter is a function which tests elements of the input type, and
returns the longest initial part of the input, all of whose elements have the
required property. For instance
ones = 1 : ones
Evaluation of this in a Haskell system produces a list of ones, indefinitely. This can
be interrupted in GHCi by typing Ctrl-C; this produces the result
[1,1,1,1,1,1,1,ˆC,1,1,1,Interrupted.
We can sensibly evaluate functions applied to ones. If we define
addFirstTwo ones
; addFirstTwo (1:ones)
; addFirstTwo (1:1:ones)
; 1+1
; 2
Built into the system are the lists [n .. ], [n,m .. ], so that
[3 .. ] = [3,4,5,6,...
[3,5 .. ] = [3,5,7,9,...
We can define these ourselves:
fromStep 3 2
; 3 : fromStep 5 2
; 3 : 5 : fromStep 7 2
; ...
Infinite lists 469
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 …
2 3 5 7 9 11 13 15 17 19 21 23 25 …
2 3 5 7 11 13 17 19 23 25 …
These functions are also defined over any instance of the Enum class; details can be
found in standard prelude.
List comprehensions can also define infinite lists. The list of all Pythagorean
triples – whole numbers which can the sides of a right-angled triangle – is given by
selecting z in [2 .. ], and then selecting suitable values of x and y below that.
pythagTriples =
[ (x,y,z) | z <- [2 .. ] , y <- [2 .. z-1] ,
x <- [2 .. y-1] , x*x + y*y == z*z ]
pythagTriples
= [(3,4,5),(6,8,10),(5,12,13),(9,12,15),(8,15,17),(12,16,20),...
and this is a special case of the prelude function iterate, which gives the infinite
list
[ x , f x , .. , fn x , ..
Example
1. Generating prime numbers
A positive integer greater than one is prime if it is divisible only by itself and one.
The Sieve of Eratosthenes – an algorithm known for over two thousand years – works
by cancelling out all the multiples of numbers, once they are established as prime.
The primes are the only elements which remain in the list. The process is illustrated
in Figure 17.2.
We begin with the list of numbers starting at 2. The head is 2, and we remove
all the multiples of 2 from the list. The head of the remainder of the list, 3, is prime,
since it was not removed in the sieve by 2. We therefore sieve the remainder of the
470 CHAPTER 17. LAZY PROGRAMMING
primes = sieve [2 .. ]
sieve (x:xs) = x : sieve [ y | y <- xs , y ‘mod‘ x > 0]
where we test whether x divides y by evaluating y ‘mod‘ x; y is a multiple of x if
this value is zero. Beginning the evaluation, we have
primes
; sieve [2 .. ]
; 2 : sieve [ y | y <- [3 .. ] , y ‘mod‘ 2 > 0]
; 2 : sieve (3 : [ y | y <- [4 .. ] , y ‘mod‘ 2 > 0])
; 2 : 3 : sieve [ z | z <- [ y | y <- [4 .. ] , y ‘mod‘ 2 > 0],
z ‘mod‘ 3 > 0]
; ...
; 2 : 3 : sieve [ z | z <- [5,7,9...] , z ‘mod‘ 3 > 0]
; ...
; 2 : 3 : sieve [5,7,11,...]
; ...
Can we use primes to test for a number being a prime? If we evaluate member
primes 7 we get the response True, while member primes 6 gives no answer. This
is because an infinite number of elements have to be checked before we conclude
that 6 is not in the list. The problem is that member cannot use the fact that primes
is ordered. This we do in memberOrd.
memberOrd :: Ord a => [a] -> a -> Bool
memberOrd (x:xs) n
| x<n = memberOrd xs n
| x==n = True
| otherwise = False
The difference here is in the final case: if the head of the list (x) is greater than the
element we seek (n), the element cannot be a member of the ordered list. Evaluating
the test again,
memberOrd [2,3,5,7,11,...] 6
; memberOrd [3,5,7,11,...] 6
; memberOrd [5,7,11,...] 6
; memberOrd [7,11,...] 6
; False
Many computer systems require us to generate ‘random’ numbers, one after an-
other. Our queuing simulation is a particular example upon which we focus here,
after looking at the basics of the problem.
Infinite lists 471
No Haskell program can produce a truly random sequence; after all, we want
to be able to predict the behaviour of our programs, and randomness is inherently
unpredictable. What we can do, however, is generate a pseudo-random sequence
of natural numbers, smaller than modulus. This linear congruential method works
by starting with a seed, and then by getting the next element of the sequence from
the previous value thus
seed = 17489
multiplier = 25173
increment = 13849
modulus = 65536
the sequence produced by randomSequence seed begins
[17489,59134,9327,52468,43805,8378,...
The numbers in this sequence, which range from 0 to 65535, all occur with the same
frequency. What are we to do if instead we want the numbers to come in the (integer)
range a to b inclusive? We need to scale the sequence, which is achieved by a map:
so that numbers in the range 0 to 65535 are transformed into items of type a. The
idea of the function is to give the following ranges to the entries for the list above.
The makeFun function has an extra argument, which carries the position in the range
0 to modulus-1 reached so far in the search; it is initially zero. The fromIntegral
function used here converts an Int to an equivalent Float.
The transformation of a list of random numbers is given by
Exercises
factorial = [1,1,2,6,24,120,720,...]
fibonacci = [0,1,1,2,3,5,8,13,21,...]
pythagTriples2 =
= [ (x,y,z) | x <- [2 .. ] ,
y <- [x+1 .. ] ,
z <- [y+1 .. ] ,
x*x + y*y == z*z ]
The problem is in the order of choice of the elements. The first choice for x is 2, and
for y is 3; given this, there are an infinite number of values to try for z: 4, 5 and so on,
indefinitely. We therefore never try any of the other choices for x or y, among which
the triples lie.
Two options present themselves. First we can redefine the solution, as in the original
pythagTriples, so that it involves only one infinite list. Alternatively, we can try to
write a function which returns all pairs of elements from two infinite lists:
which returns a list containing the factors of a positive integer. For instance,
factors 12 = [1,2,3,4,6,12]
Using this function or otherwise, define the list of numbers whose only prime
factors are 2, 3 and 5, the so-called Hamming numbers:
hamming = [1,2,3,4,5,6,8,9,10,12,15,...
of a list
17.26 Define the function infiniteProduct specified above, and use it to correct
the definition of pythagTriples2.
474 CHAPTER 17. LAZY PROGRAMMING
iterate f x map g
x, y, z, …
x, f x, f(f x), …
g x, g y, g z, …
iterate f x map g
x, y, z, … x, x+y, x+y+z, …
iList
zipWith (+) (0:) out
0, x, x+y, x+y+z, …
Among the exercises in the last section was the problem of finding the running
sums
Exercises
17.27 Give a definition of the list [ 2ˆn | n <- [0 .. ] ] using a process net-
work based on scanl’. (Hint: you can take the example of factorial as a guide.)
17.28 How would you select certain elements of an infinite list? For instance, how
would you keep running sums of the positive numbers in a list of numbers?
17.29 How would you merge two infinite lists, assuming that they are sorted? How
would you remove duplicates from the list which results? As an example, how
would you merge the lists of powers of 2 and 3?
17.8. CASE STUDY: SIMULATION 477
• Section 14.5, where we designed the algebraic types Inmess and Outmess,
• Section 16.5, where the abstract types QueueState and ServerState were
introduced, and in
• Section 17.6, where we showed how to generate an infinite list of pseudo-
random waiting times chosen according to a distribution over the times 1 to
6.
As we said in Section 14.5, our top-level simulation will be a function from a series
of input messages to a series of output messages, so
randomTimes
= map (makeFunction dist . fromIntegral) (randomSequence seed)
; [2,5,1,4,3,1,2,5,...
478 CHAPTER 17. LAZY PROGRAMMING
We are to have arrivals of one person per minute, so the input messages we generate
are
simulationInput
= zipWith Yes [1 .. ] randomTimes
; [ Yes 1 2 , Yes 2 5 , Yes 3 1 , Yes 4 4 , Yes 5 3 ,...
What are the outputs produced when we run the simulation on this input with four
queues, by setting the constant numQueues to 4? The output begins
Experimenting
We now have the facilities to begin experimenting with different data, such as the
distribution and the number of queues. The total waiting time for a (finite) sequence
of Outmess is given by
Undefinedness
In nearly every programming language, it is possible to write a program which fails
to terminate, and Haskell is no exception. We call the value of such programs the
undefined value, as it gives no result to a computation.
The simplest expression which gives an undefined result is
undef :: a
undef = undef (undef.1)
which gives a non-terminating or undefined value of every type, but of course we
can write an undefined program without intending to, as in
list1 = 2:3:undef
The list has a well-defined head, 2, and tail 3:undef. Similarly, the tail has a head,
3, but its tail is undefined. The type [Integer] therefore contains partial lists like
list1, built from the undefined list, undef, parts of which are defined and parts of
which are not.
Of course, there are also undefined integers, so we also include in [Integer]
lists like
list2 = undef:[2,3]
list3 = undef:4:undef
which contain undefined values, and might also be partial. Note that in list3 the
first occurrence of undef is at type Integer while the second is at type [Integer].
480 CHAPTER 17. LAZY PROGRAMMING
What happens when a function is applied to undef? We use the rules for cal-
culation we have seen already, so that the const function of the standard prelude
satisfies
const 17 undef ; 17
If the function applied to undef has to pattern match, then the result of the function
will be undef, since the pattern match has to look at the structure of undef, which
will never terminate. For instance, for the functions used in Chapter 9,
In writing proofs earlier in the book we were careful to state that in some cases the
results hold only for defined values.
An integer is defined if it is not equal to undef; a list is defined if it is a finite list
of defined values; using this as a model it is not difficult to give a definition of the
defined values of any algebraic type.
A finite list as we have defined it may contain undefined values. Note that in
some earlier proofs we stipulated that the results hold only for (finite) lists of defined
values, that is for defined lists.
To prove the property P(xs) for all finite or partial lists (fp-lists) xs we have to do
three things:
Among the results we proved by structural induction in Chapter 9 were the equa-
tions
for all finite lists xs, ys and zs. For these results to hold for all fp-lists, we need to
show that
as well as being sure that the induction step is valid for all fp-lists. Now, by (sum.u)
and (doubleAll.u) the equation (sum-double.u) holds, and so (sum-double)
holds for all fp-lists. In a similar way, we can show (assoc++.u). More interesting
is (reverse++.u). Recall the definition of reverse:
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]
It is clear from this that since there is a pattern match on the parameter, undef as
the first parameter will give an undef result, so
This is enough to show that (reverse++.u) does not hold, and that we cannot infer
that (reverse++) holds for all fp-lists. Indeed the example above shows exactly that
(reverse++) is not valid.
Infinite lists
Beside the fp-lists, there are infinite members of the list types. How can we prove
properties of infinite lists? A hint is given by our discussion of printing the results of
evaluating an infinite list. In practice what happens is that we interrupt evaluation
by hitting Ctrl-C after some period of time. We can think of what we see on the
screen as an approximation to the infinite list.
If what we see are the elements a0 ,a1 ,a2 ,...,an , we can think of the approx-
imation being the list
are approximations to the infinite list [a0 ,a1 ,a2 ,...,an ,...].
Two lists xs and ys are equal if all their approximants are equal, that is for all
natural numbers n, take n xs = take n ys. (The take function gives the de-
fined portion of the nth approximant, and it is enough to compare these parts.) A
more usable version of this principle applies to infinite lists only.
482 CHAPTER 17. LAZY PROGRAMMING
Example
Two factorial lists
Our example here is inspired by the process-based programs of Section 17.7. If fac
is the factorial function
fac :: Int -> Int
fac 0 = 1 (fac.1)
fac m = m * fac (m-1) (fac.2)
one way of defining the infinite list of factorials is
facMap = map fac [0 .. ] (facMap.1)
while a process-based solution is
facs = 1 : zipWith (*) [1 .. ] facs (facs.1)
Assuming these lists are infinite (which they clearly are), we have to prove for all
natural numbers n that
facMap!!n = facs!!n (facMap.!!)
Proof In our proof we will assume for all natural numbers n the results
(map f xs)!!n = f (xs!!n) (map.!!)
(zipWith g xs ys)!!n = g (xs!!n) (ys!!n) (zipWith.!!)
which we discuss again later in this section.
(facMap.!!) is proved by mathematical induction, that is we prove the result
for 0 outright, and we prove the result for a positive n assuming the result for n-1.
Base We start by proving the result at zero. Examining the left-hand side first,
facMap!!0
= (map fac [0 .. ])!!0 by (facMap.1)
= fac ([0 .. ]!!0) by (map.!!)
= fac 0 by def of [0 .. ],!!
= 1 by (fac.1)
The right-hand side is
facs!!0
= (1 : zipWith (*) [1 .. ] facs)!!0 by (facs.1)
= 1 by def of !!
thus establishing the base case.
17.9. PROOF REVISITED 483
Induction In the induction case we have to prove (facMap.!!) using the in-
duction hypothesis:
facMap!!n
= (map fac [0 .. ])!!n by (facMap.1)
= fac ([0 .. ]!!n) by (map.!!)
= fac n by def of [0 .. ],!!
= n * fac (n-1) by (fac.2)
It is not hard to see that we have facMap !! (n-1) = fac (n-1) by a similar ar-
gument to the first three steps here and so,
= n * (facMap!!(n-1))
facs!!n
= (1 : zipWith (*) [1 .. ] facs)!!n by (facs.1)
= (zipWith (*) [1 .. ] facs)!!(n-1) by def of !!
= (*) ([1 .. ]!!(n-1)) (facs!!(n-1)) by (zipWith.!!)
= ([1 .. ]!!(n-1)) * (facs!!(n-1)) by def of (*)
= n * (facs!!(n-1)) by def of [1 .. ],!!
= n * (facMap!!(n-1)) by (hyp)
The final step of this proof is given by the induction hypothesis, and completes the
proof of the induction step and the result itself.
Many other of the equations we proved initially for finite lists can be extended to
proof for the fp-lists, and therefore to all lists. Some of these are given in the exercises
which follow.
484 CHAPTER 17. LAZY PROGRAMMING
Further reading
The techniques we have given here provide a flavour of how to write proofs for infi-
nite lists and infinite data structures in general. We cannot give the breadth or depth
of a full presentation, but refer the reader to Paulson (1987) for more details. An al-
ternative approach to proving the factorial list example is given in Thompson (1999),
which also gives a survey of proof in functional programming.
Exercises
[Hint: you will need to choose the right variable for the induction proof.]
are infinite.
(x:_)!!0 = x
(_:xs)!!n = xs!!(n-1)
[]!!n = error "Indexing"
show that for all strict functions f, fp-lists xs and natural numbers n,
17.9. PROOF REVISITED 485
and therefore infer that the result is valid for all lists xs. State and prove a
similar result for zipWith.
(q &&& p) x = q x && p x
Summary
Lazy evaluation of Haskell expressions means that we can write programs in a differ-
ent style. A data structure created within a program execution will only be created
on demand, as we saw with the example of finding the sum of fourth powers. In find-
ing routes through a graph we saw that we could explore just that part of the graph
which is needed to reveal a path. In these and many more cases the advantage of
lazy evaluation is to give programs whose purpose is clear and whose execution is
efficient.
We re-examined the list comprehension notation, which makes many list pro-
cessing programs easier to express; we saw this in the particular examples of route
finding and parsing.
A design principle exploited in this chapter involved the use of lazy lists: if a
function can return multiple results it is possible to represent this as a list; using
lazy evaluation, the multiple results will only be generated one-by-one, as they are
required. Also, we are able to represent ‘no result’ by the empty list, []. This ‘list of
successes’ method is useful in a variety of contexts.
Exploiting this principle as well as higher-order functions, polymorphism and
list comprehensions we gave a library of parsing functions, which we saw applied
to the type of arithmetical expressions, Expr. This showed one of the strengths of
modern functional programming languages, whose constructs are especially well
suited to describing general toolkits of this sort.
Rather than being simply a curiosity, this chapter has shown that we can exploit
infinite lists for a variety of purposes.
The chapter concluded with a discussion of how proofs could be lifted to the
partial and infinite elements of the list type: criteria were given in both cases and we
gave examples and counter-examples in illustration.
Chapter 18
This chapter looks again at I/O in Haskell, particularly focussing on the do notation
which we use to write I/O programs. We’ll see that we can write all sorts of different
programs using do notation, including programs that manipulate state, that non-
deterministically return more than one result, as well as programs that may fail, or
raise exceptions.
Underlying each of these is a monad. A monad provides the infrastructure for
sequencing and naming used in the do notation, and we will show instances of the
Monad class for lists, the Maybe type, a parsing monad and more.
As well as looking at these definitions, we will program a number of examples,
showing how a monadic style, using do, makes programs more readable and flexible.
Each instance of a monad gives a ‘little language’ for writing programs, and we’ll
come back to look at this in detail in the next chapter.
readWrite =
do
487
488 CHAPTER 18. PROGRAMMING WITH MONADS
getLine
putStrLn "one line read"
where the two statements are sequenced one after the other. It is also possible to
name the results of statements for use in the program, so that we can name the line
read, and output it, like this:
readEcho :: IO ()
readEcho =
do
line <-getLine
putStrLn ("line read: " ++ line)
In this program line <- getLine names the result of the getLine and so it can be
echoed in the next statement.
sumInts s
= do n <- getInt
if n==0
then return s
else sumInts (s+n)
In writing the program there are two cases: we first read an integer value, using the
getInt function defined in Chapter 8. If we read zero then the result must be the
sum so far, s; if not, we get the result by calling sumInts again, with the parameter
set to (s+n).
The program is written in the style we saw in Chapter 8: the function is tail-
recursive – being called as the last statement in the program – and the loop data is
used to carry the “sum so far”: here s and s+n.
It is interesting with compare the definition of sumInts with the recursion in
sumAcc s [] = s
sumAcc s (n:ns)
= if n==0
then s
else sumAcc (s+n) ns
Here we use the first variable to sumAcc as an accumulator where the “result so far”
can be held.
18.1. I/O PROGRAMMING 489
We can also put the sumInts program inside a ‘wrapper’ which explains its pur-
pose and prints the sum at the end.
sumInteract :: IO ()
sumInteract
= do putStrLn "Enter integers one per line"
putStrLn "These will be summed until zero is entered"
sum <- sumInts 0
putStr "The sum is "
print sum
Exercises
sumInts :: IO Integer
sumInts
= do n <- getInt
if n==0
then return 0
else (do m <- sumInts
return (n+m))
so that repeat test oper has the effect of repeating oper until the condi-
tion test is True.
18.4 Define the higher-order function while in which the condition and the oper-
ation work over values of type a. Its type should be
18.5 Using the function whileG or otherwise, define an interaction which reads a
number, n say, and then reads a further n numbers and finally returns their
average.
490 CHAPTER 18. PROGRAMMING WITH MONADS
18.6 Modify your answer to the previous question so that if the end of file is reached
before n numbers have been read, a message to that effect is printed.
which performs the interactions in turn, but discards their results. Finally,
show how you would sequence a series, passing values from one to the next:
copyInteract :: IO ()
copyInteract =
do
hSetBuffering stdin LineBuffering
copyEOF
hSetBuffering stdin NoBuffering
where we can see that the buffering mode can be changed within an I/O program.
The copyEOF program itself is given by
18.2. FURTHER I/O 491
copyEOF :: IO ()
copyEOF =
do
eof <- isEOF
if eof
then return ()
else do line <- getLine
putStrLn line
copyEOF
where isEOF :: IO Bool is an I/O program to determine whether or not the end
of standard input has been reached.
Give this a try within GHCi: you will find that you can use Ctrl-D to terminate
your input if you run copyInteract; if on the other hand you run copyEOF directly,
you will need to interrupt the program using Ctrl-C.
File I/O
As well as reading and writing to a terminal, the Haskell I/O model also provides for
reading from and writing and appending to files, by means of the functions
where
and files are specified by the text strings appropriate to the implementation in ques-
tion.
Errors
I/O programs can raise errors, which belong to the system-dependent data type
IOError. The function
builds an I/O action which fails giving the appropriate error, and the program
will catch an error raised by the first argument and handle it using the second ar-
gument, which gives a handler – that is an action of type IO a – for each possible
IOError.
492 CHAPTER 18. PROGRAMMING WITH MONADS
getContents :: IO String
a primitive to get the contents of the standard input, and which is used in this func-
tion, also defined in System.IO
sRandom :: Strategy
sRandom _ = convertToMove (randInt 3)
Here we use the unsafePerformIO function to extract the result from the IO monad.
As its name suggests this is (potentially) unsafe, and should be used with care; some
would go further, and say that it should never be used.
If we want to avoid unsafePerformIO we need to look again at the design of our
case study. In Chapter 8 defined strategies to belong to the type
Exercises
18.8 Write a file-handling version of the program sumInts, which reads the integer
sequence from a file.
18.9 Write a lazy-list version of the program sumInts, which you can then run using
interact.
18.11 [Harder] Reimplement the Rock - Paper - Scissors case study to use the monadic
strategies in StrategyM.
initial :: Store
value :: Store -> Var -> Integer
update :: Store -> Var -> Integer -> Store
which is used to parse each line of input into a Command. For instance,
eval (Lit n) st = n
eval (Var v) st = value st v
eval (Op op e1 e2) st
= opValue op v1 v2
where
v1 = eval e1 st
v2 = eval e2 st
where the opValue function of type Ops->Integer->Integer->Integer interprets
each operator, such as Add, as the corresponding function, like (+).
What is the effect of a command? An expression should return the value of the
expression in the current store; an assignment will change the store, and a null com-
mand will do nothing. We therefore define a function which returns both a value
and a store,
= (val , newSt)
where
val = eval e st
newSt = update st v val
A single step of the calculator will take a starting Store, and read an input line,
evaluate the command in the line, print some output and finally return an updated
Store,
calcStep :: Store -> IO Store
calcStep st
= do line <- getLine
let comm = commLine line (1)
let (val,newSt) = command comm st (2)
print val
return newSt
In lines (1) and (2) of the definition of calcStep we see an example of the use of
let within a do expression. Line (1),
let comm = commLine line
gives comm the value of parsing the line, and this is subsequently used in (2),
let (val,newSt) = command comm st
which simultaneously gives val and newSt the value of the expression read and the
new state. In the lines that follow the lets, the value val is printed and the new state
newSt is returned as the overall result of the interaction. A sequence of calculator
steps is given by
calcSteps :: Store -> IO ()
calcSteps st =
do
eof <- isEOF
if eof
then return ()
else do newSt <- calcStep st
calcSteps newSt
The main I/O program for the calculator is given by starting off calcSteps with the
initial store, operating in modified buffering mode
mainCalc :: IO ()
mainCalc =
do
hSetBuffering stdin LineBuffering
calcSteps initial
hSetBuffering stdin NoBuffering
496 CHAPTER 18. PROGRAMMING WITH MONADS
In the exercises various extensions and modifications of the calculator program are
discussed.
Exercises
18.12 How would you add initial and final messages to the output of the calculator?
18.13 Discuss how you would have to modify the system to allow variables to have
arbitrarily long names, consisting of letters and numbers, starting with a letter.
18.14 How would you extend the calculator to deal with decimal floating-point num-
bers as well as integers?
18.15 Discuss how you would modify the calculator so that it could read input com-
mands split over more than one line. You will need to decide how this sort
of split is signalled by the user – maybe by \ at the end of the line – and how
to modify the interaction program to accommodate this. Alternatively, you
might let the user do this without signalling; can you modify the program to
do that?
18.16 How would you modify the parser so that ‘white space’ is permitted in the
input commands, as in the example
return :: a -> IO a
putStr :: String -> IO ()
getLine :: IO String
but also items of the IO a type can be sequenced using the do construct. In this
section we look ‘under the bonnet’ to see how the do works, as this will lead to us
seeing IO as just one example of a general phenomenon.
The key to understanding the do is the operation (»=), which is often pronounced
‘bind’, which sequences two operations, one after the other, binding the result of the
first and making it available as a parameter to the second.
IO a
with a function taking the result of this (of type a) into an IO b, that is an object of
type a -> IO b,
a IO b
We can join them together, passing the result of the first as an argument to the sec-
ond, thus:
IO a a IO b
The result of putting them together is something which does some I/O before re-
turning a value of type b:
IO IO b
where recall that \x -> e is the function which takes the parameter x to result e, so
here the parameter is called line, and used just as above. More complex examples
are translated in a similar way.
As you can see from this simple example, using (»=) makes programs more dif-
ficult to read and understand: the do notation highlights the essential features of I/O
programming:
and gives a readable and comprehensible way of writing these programs within Haskell.
We’ll continue to use the do notation, but will keep in mind that it essentially
boils down to the existence of a function (»=) which does the work of sequencing
I/O programs, and binding their results for future use.
Exercises
18.17 Repeat some of the earlier examples and exercises using the »= operator in-
stead of a do expression.
The IO types give one kind of program which we can describe with the do notation,
but there are other kinds of program we might want to write in a similar way, such
as
Well, we can do this, so long as the types have the analogue of the bind and return
functions. In other words, we need to implement functions in a particular kind of
interface, and we know from Chapter 13 that this is given in Haskell by the Monad
class, like this:
18.5. MONADS: LANGUAGES FOR FUNCTIONAL PROGRAMMING 499
Once we have an instance of this class we can use the do notation over the type.
Let’s look at the example of programs that might fail, second in the list of exam-
ples above. We looked at these in Section 14.4 where we saw that the Maybe type was
a good model for values including a potential failure:
and we developed a number of functions which worked with Maybe types to transmit
errors through the code. Alternatively we can use the do notation, because we have
this instance for Monad:
where we are using the braces ‘{’, ‘}’ and explicit separator ‘:’ to put the statements
in the do program on a single line, as first introduced in Chapter 4. In a similar way,
lists form a monad:
From this example we can see that lists model non-deterministic computation: each
of x and y have two possible values, and that gives four possibilities for (x+y). The
definition of (»=) over lists reflects this: f is applied to all the possibilities in xs and
the results are concatenated together.
In the next section we fill in some of the formalities about monads, but this latter
section can be skipped on first reading, if you wish. We then look at some more
examples of monads.
500 CHAPTER 18. PROGRAMMING WITH MONADS
Why ‘monad’?
Why is this type class called ‘monad’? The name originates in category theory, which
was used by Eugenio Moggi to build mathematical models of different kinds of com-
putations. However, that doesn’t mean that you need to learn category theory to use
them, any more than you need to understand the intricacies of modern micropro-
cessors to use your computer! Understanding that the do notation gives sequencing
and binding (or naming) is all you need to understand to get started.
Monads, formally
A monad is a family of types m a, based on a polymorphic type constructor m, with
functions return, (»=), (»), and fail:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
(>>) :: m a -> m b -> m b
fail :: String -> m a
This is an example of a class, whose instances are type constructors – that is func-
tions which build types from types – rather than types. Examples of type construc-
tors are ‘list’, written [] in Haskell, and IO as we have seen already.
The definition of Monad also contains default declarations for » and fail:
m >> k = m >>= \_ -> k
fail s = error s
From this definition it can be seen that » acts like »=, except that the value returned
by the first argument is discarded rather than being passed to the second argument.
In order properly to be a monad, the functions return and (»=) and the value
zero should have some simple properties. Informally we can state the requirements
as follows.
• The operation return x should simply return the value x, without any ad-
ditional computational effect, such as input or output in the case of the IO
monad.
• The value fail s corresponds to a computation which fails, giving the error
message s.
The laws are much clearer when stated in terms of a derived operator, >@>.
(>@>) :: Monad m => (a -> m b) ->
(b -> m c) ->
(a -> m c)
a m b b m c
to give
a m c
m >>= f = do { x <- m; f x }
do { y <- return x; f y } = f x
do { x <- m; return x } = m
and the third is implicit in the fact that the do construct is associative.
A more substantial example is given by parsing, where we can show that Parse a
is a monad. To make a formal declaration of this we need to wrap it in a newtype
constructor, MP, which clutters the definition a bit.
1 In category theory, this operation is called Kleisli composition.
502 CHAPTER 18. PROGRAMMING WITH MONADS
newtype MP a b = MP { mp :: (Parse a b) }
The identity monad, which takes a type to itself, is the simplest example of a monad,
with the definitions
x >>= f = f x
return = id
18.5. MONADS: LANGUAGES FOR FUNCTIONAL PROGRAMMING 503
In order for this to be a legal Haskell definition we need to define the identity type
constructor, and, within Haskell 2010, any instance declaration needs to be for a
data type, newtype or primitive type. So, we define, as in Control.Monad.Identity,
newtype Identity a = Identity identity :: a
where the constructor Identity ‘wraps’ the value up, and the field name, identity
is used to ‘unwrap’ them.. Now we can define the instance itself,
instance Monad Identity where
(Identity x) >>= f = f x -- x >>= f = f x
return a = Identity a -- return = id
We can then run things in this monad, if we give a Show instance for Identity
instance Show a => Show (Identity a) where
show (Identity x) = show x
as in this example, which also shows how we can give multi-line input to GHCi in-
teractively.
*UseMonads> :{
*UseMonads | do { x <- return ’c’ :: Identity Char;
*UseMonads | y <- return ’d’;
*UseMonads | return [x,y] }
*UseMonads | :}
"cd"
Computationally, this monad represents the trivial state in which no actions are per-
formed, and values are returned immediately.
Later in this chapter we will give an example of a state monad, State a b. An op-
eration of this type can change the state (of type a) before returning a value of type
b.
Over lists these functions are called map and concat; many of the properties of map
and concat over lists lift to these functions. For instance, we can show using prop-
erties (M1) to (M3) that for all f and g
In fact, fmap is defined over a wider set of types that monads, and it is the function
that characterises the Functor class:
Example instances of Functor which are not monads include (a,a), ([a],[a])
and Tree a (from the next section).
Exercises
18.18 Show that sets and binary trees can be given a monad structure, as can the
type:
18.19 For the monads Id , [] and Maybe prove the rules (M1) to (M3). Also show
that these rules hold for your implementations in the previous exercise.
18.20 Prove the property (M4) using the laws (M1) to (M3).
18.22 Can you define a different monad structure over lists from that given above?
Check that your definition has properties (M1) to (M3).
18.23 Write down the definitions of map and join over lists using list comprehen-
sions. Compare them with the definitions of fmap and join given in the do
notation in this section.
18.24 Re-implement the parser for the calculator using the do construct (based on
»=) rather than (>*>) and build. Contrast the two approaches.
18.25 Show that the type functions (a,a), ([a],[a]) and Tree a are instances of
Functor.
18.6. EXAMPLE: MONADIC COMPUTATION OVER TREES 505
How is the definition structured? We put the operations in sequence, using do. First
we return the value n, giving it the name num. Next we calculate sumTree t1 and
sumTree t2, naming their results s1 and s2. Finally we return the result, which is
the sum num+s1+s2.
Now, since all we are doing here is calculating values and not trying to do any
I/O or other side-effecting operation, we make the monad St the identity monad
Identity which we mentioned earlier, so we have
num := n ;
s1 := sumTree t1 ;
s2 := sumTree t2 ;
return (num + s1 + s2) ;
where ‘num :=’ corresponds to the ‘<-’ and do puts a sequence of commands one
after the other, as does the semi-colon. Remember, though, that this is a single-
assignment language, so that num, s1 and so on are names not variables!
To give a function of type Tree Integer -> Integer we compose with the
identity function to give
identity . sumTree
where
takes the wrapper off an element Identity x to give the element x. In the next
section we tackle a more complex problem, but see the same monadic structure
repeated.
so that given an arbitrary tree we transform it to a tree of integers in which the orig-
inal elements are replaced by natural numbers, starting from 0. An example is given
in Figure 18.1. The same element has to be replaced by the same number at every
occurrence, and when we meet an as-yet-unvisited element we have to find a ‘new’
number to match it with.
18.6. EXAMPLE: MONADIC COMPUTATION OVER TREES 507
Moon 0
Ahmet Dweezil 1 2
Ahmet Moon 1 0
This has given us the monad; all that remains is to define the function numberNode.
Our definition is
numberNode :: Eq a => a -> State a Integer
numberNode x = State (nNode x)
Standing back, we can see that we have completed our definition of the function
numberTree exampleTree
In order to extract the result of running the program, we have to write a function
This has to perform the calculation, starting with some initial table, and return the
resulting value of type b. The definition is
where we see that st is applied to the initial state []. The result of this is a pair, from
which we select the second part, of type b. Now we can define our function
Discussion
Taking it further
What we have presented here just scratches the surface of monadic programming in
Haskell. We have covered the basic monads, but not looked how multiple monads
– such as state and exceptions – can be combined into a single monad: this is done
using monad transformers, for which a number of libraries are available.
We have also not looked in any detail at monadic approaches to parsing, be-
yond showing how our earlier, non-monadic, parser combinators could support a
monadic view too. Most of the available Haskell parsing libraries – which extend the
basic approach in both expressivity and efficiency – are monadic.
There are extensions to the Monad interface: many monads, including lists and
the Maybe monad, have a natural notion of a sum and zero: this gives the MonadPlus
interface. At the same time, there are various weakenings of the notion of monad,
including applicative functors and arrows, each with their own advantages and dis-
advantages.
There is no shortage of online tutorials on monads and these other features, and
other texts cover them in more detail too. A standout among these is Peyton Jones’
tutorial (Peyton Jones 2001) on the ‘awkward squad’ of monads that support IO, ex-
ceptions and other computational effects.
Exercises
18.26 Give a non-monadic definition of numTree, threading the state through the
computation explicitly; compare your solution with the monadic version given
here.
where the extra integer parameter carries the current ‘offset’ into the list.
18.28 Show how you can use a State-style monad in a computation to replace each
element in a tree by a random integer, generated using the techniques of Sec-
tion 17.6.
18.29 We can use monads to extend the case study of the calculator in a variety of
ways. Consider how you would
18.6. EXAMPLE: MONADIC COMPUTATION OVER TREES 511
Summary
In this chapter we have seen how the do notation, which we first saw used for I/O
programming, can be used to support a whole lot of different kinds of program: non-
deterministic, stateful, parsing and so on. The mechanism which underlies the do
notation is the Monad class, which embodies the functionality needed to sequence
sub-programs, and to name their results for use later on in the program.
Like any abstract data type interface, the advantage of programming against
the interface is that it is possible to modify the underlying implementation without
changing the higher-level program. We saw this in action in the final section of the
chapter, where two very different computations over a tree had the same top-level
structure, described in a monad.
We will come back to monads in the next chapter, where we look at how Haskell is
a host for ‘little languages’ that describe how to compute in a particular application
area.
512 CHAPTER 18. PROGRAMMING WITH MONADS
Chapter 19
Domain-Specific Languages
One area where Haskell has been particularly successful is in building implementa-
tions of domain-specific languages (DSLs). Haskell algebraic types give a straightfor-
ward representation of language structure, and the rich collection of types, includ-
ing functions as data, give us great flexibility in modelling phenomena, as we saw,
for instance, with strategies in Rock - Paper - Scissors being modelled as functions.
Moreover monads provide an additional expressive power.
This chapter first looks at what DSLs are, and why they are important. We then
ask what it means to be a DSL in Haskell, and cover the different approaches to writ-
ing Haskell DSLs, both as combinator libraries and as monadic languages. We also
discuss different types of embedding – shallow and deep – and contrast how these
approaches apply in particular case studies, including pictures and regular expres-
sions. We also revisit QuickCheck as a DSL for generating random data. We conclude
by looking at a number of examples of practical Haskell DSLs.
• To use our program we need to run it, and to run a program on hardware, like
an intel chip, we need to translate the high-level program into machine code
that that chip can execute. A compiler like GHC works by successively trans-
forming a source program written in Haskell into Haskell Core, a stripped-
down functional programming language, then into STG, code for an abstract
machine. Finally, via C--, a C variant designed as ‘high-level machine code’,
programs in C, LLVM and machine code are the output, as illustrated in Fig-
ure 19.1.
513
514 CHAPTER 19. DOMAIN-SPECIFIC LANGUAGES
Machine
Haskell Core STG C- -
code
LLVM
fectively we have ‘languages all the way down’1 to the hardware providing the
communication.
Domain-specific languages
Haskell, Java, C, C# and so forth are general purpose programming languages: in
principle we can program anything we like in Haskell, Java or C. Other languages
are designed to work in particular application areas: we call these domain-specific
languages or DSLs. Let’s look at some examples.
• VHDL and Lava are DSLs for hardware design: instead of data types for strings,
lists and so forth, the languages have data types representing transistors, logic
gates, signals and so on. The results of processing them are circuit designs and
layouts, to be fed to fabricators.
• This book is being written using LaTeX, a text processing language. I write
commands to express layout and other configurations and these, together with
pictures (see below), get processed into a PDF file. PDF itself is a low-level
language that also gets processed into bitmaps (on a display of some sort) or
marks on paper (by printing).
• We can see the spreadsheet language of Excel as the most widely used func-
tional programming language in the world. Excel formulas describe how val-
1 See the entry for "Turtles all the way down" in Wikipedia.
19.1. PROGRAMMING LANGUAGES EVERYWHERE 515
Programming Language
DSL DSL
Model Model
World World
Stand-alone Embedded
ues in one call are calculated from values in others, and re-computing values
is done automatically when values change.
Some DSLs serve to model the world: an Excel spreadsheet embodying a business
plan will allow us to forecast future profits, for example. Others more directly affect
the world: a DSL for robotic control will make machines move, while a VHDL design
will ultimately become a complex physical artifact.
DSLs like VHDL, LaTeX and SVG have many of the features of general purpose
languages. Their programs have structure, they have simple data types – like num-
bers, as well as ways of naming sub-components for re-use: think of the ‘cut and
paste’ to replicate a formula in Excel, or named circuits in VHDL.
On the other hand, they also have special types and constructs designed to make
them work particularly well in their own domain. So, if you want to write a book,
draw a picture, design a circuit or work out your cash flow, you will most likely use
the DSL rather than starting from scratch to program the solution in Haskell or Java.
and we have to parse or translate them into Haskell to interpret them. Another way
of seeing that this language is stand-alone is the fact that we could choose to im-
plement it in any programming language whatever: the process would still involve
parsing strings like "s:(23-s)" into that programming language to work with them.
Another example of a stand-alone DSL would be a language for pictures with expres-
sions like:
Put pic1 above pic2, and this picture beside a copy of itself.
The advantages of implementing a stand-alone DSL are that we have complete
control of the syntax of the language, and also of its semantics – that is what pro-
grams in the language mean. We can also have complete control of any error mes-
sages that we send back to a user if things go wrong in some way: for instance if an
expression is mal-formed.
On the other hand, we have to build the implementation up from scratch: the
string "23" has nothing intrinsically to do with the number 23, until our implemen-
tation defines that we should interpret that string as that particular number.
On the other hand, a DSL is embedded if it builds on features of the host lan-
guage in which it is written. Going back to the examples, we can build a language for
expressions using algebraic data types, building on the numbers built into Haskell.
Doing this, we get numerical expressions like this
Assign s (Op Sub (Lit 23) s)
and for pictures, we can use the normal syntax for function application and local
definitions to describe pictures this way:
let
pic = pic1 ‘above‘ pic2 (pic)
in
pic ‘beside‘ pic
The advantage of the embedded approach is that we can ‘piggy back’ on the facilities
of Haskell to make the language implementation much more straightforward. To use
the language a user doesn’t necessarily need to understand the whole of Haskell: for
instance putting together pictures just needs let and (infix) function application.
On the other hand, we do lose the freedom to completely determine the syntax:
for Haskell functions to be infix they do need to use ‘...‘ or operator syntax: we
can’t just say that a function should be infix. It is also possible that the error mes-
sages that come back from a DSL might ‘leak’ and say something about the host
language which is not meaningful to the user.
On balance, it is the embedded approach which has been successful in Haskell,
and that is what we’ll concentrate on here; in the remainder of the chapter, when we
say DSL, we’ll mean ‘embedded DSL’.
Purely functional. Haskell functions are really functions in the mathematics sense:
the output of a function is determined only by its inputs, and it has no side-
effects. A Haskell function can therefore be used directly to model ways in
which elements of a domain can be combined together into more complex
objects.
In the pictures example, there is a function which takes two pictures and re-
turns the picture made up of one argument above the other Contrast this with
an object-oriented programming language like Java. In OO style the combi-
nation operation would be a method on an object, which would destructively
update that object in some way.
Higher-order. Functions in Haskell are themselves data values, and so can be used
to model elements of the domain of interest. The operations which work over
the domain elements are then higher-order functions, which are often called
combinators, as they are used to combine other functions. Many DSLs in
Haskell use functions: parsers are represented by functions in Chapter 17,
strategies in Rock - Paper - Scissors are functions (Chapter 8). In both cases
there are sets of combinators too: for parsers in Section 17.5 and for strategies
in Section 12.2.
Again, this representation of functions as data is not accessible in an OO lan-
guage like Java, and even if closures are added to Java at some point in the fu-
ture it is unlikely that their will be so directly accessible as functions in Haskell.
Expressive type system. Haskell’s type system is both expressive and rigorous: it
is possible to express complex constraints on the type of functions, and for
these to be checked statically, so that no type errors occur at run time. The
extensions to the type system implemented in GHC make the language more
powerful, too.
The effect of this is to make it easier for the types of a DSL to be reflected in
the types of Haskell itself; obviously this is not always the case, since domain-
specific constraints may be of a different nature, only allowing objects of the
same ‘size’ or ‘shape’ to be combined, for instance, but Haskell is a better
choice of language than one with a weaker type system.
DSL DSL
program transformation
program
Representation
Model interpret
run
Model compile
run
World World
Shallow Deep
Shallow embeddings
We have already seen that we can build a domain-specific language for pictures;
indeed, we saw two different ways of modelling pictures in Haskell in Section 1.13.
First we saw that we could manipulate SVG pictures, as shown in Figure 1.2, and
then we saw that pictures can be represented by lists of strings. Both of these DSLs
are what are called shallow embeddings: the pictures are directly modelled by the
SVG data or the list of strings, and the operations over pictures are functions over
that data. Taking the list model, we have:
type Picture = [[Char]]
horse :: Picture
horse = ...
Deep embeddings
An alternative approach is to build a deep embedding. A deep embedding builds a
syntactic representation of pictures, like this:
data Pic = Horse |
Above Pic Pic |
Beside Pic Pic |
FlipH Pic |
FlipV Pic |
...
so that the corresponding Pic to the Picture above is
Above Horse Horse :: Picture
Once we have a representation like this, we can do a whole lot of different things
with it:
520 CHAPTER 19. DOMAIN-SPECIFIC LANGUAGES
• We can transform the Pic: we can remove redundant flips, and move all flips
inwards through the other constructors, like this:
(Note that equation (†) is designed to put all FLipVs inside the FlipH con-
structors).
The result of this process is that we have located the flip operations at the pic-
tures at the leaves of the tree. For many of these we will have efficient mech-
anisms for performing the operation: for example, over a symmetrical picture
we need do nothing.
• As well as transforming the Pic representation we can also analyze it. For
instance, if we had the ability to overlay one picture on top of another, it would
be possible to analyse whether the top picture completely overlaid the bottom
one, and so whether the bottom one could be completely discarded from the
representation.
• Finally, because we have a representation of domain objects, we can directly
compile these into executable ‘machine code’. In the case of Pic this might be
a compilation into PDF or PostScript, which can then be printed directly.
Shallow or deep?
If you are going to implement a DSL, should you make it shallow or deep? The ad-
vantages of the shallow embedding are, first, that it is a simple implementation of
the semantics of the domain that you are interested in. Secondly, if we want to add
new operations to the language, that’s straightforward, as we saw when we discussed
how to use an embedded DSL earlier.
On the other hand, a deep embedding allows us to do much more than sim-
ply manipulating the domain: we’re able to manipulate representations, and so to
transform, compile etc. On the other hand, extending a deep embedding is more
complex: representation and all the interpretation, transformation and analysis all
need to be extended to handle and addition to the representation.
This is a typical example of a shallow DSL, mapping the domain directly to some-
thing that models their behaviour: here a function which returns True on those
strings which match the pattern.
It is typical of a Haskell DSL because we have used functions to represent individ-
ual regular expressions, with higher-order functions, or combinators, representing
the ways that regular expressions can be combined together, such as
However, all we can do with this DSL is to check pattern matches, whereas a deep
embedding allows us more. A deep embedding would be based on a data type defi-
nition like this
infixr 7 :*:
infixr 5 :|:
data RE = Eps |
Ch Char |
RE :|: RE |
RE :*: RE |
St RE |
Plus RE
deriving(Eq,Show)
522 CHAPTER 19. DOMAIN-SPECIFIC LANGUAGES
where we have made fixity declarations which reflect the fixity of the operators, St
binding more tightly than :*:, which binds more tightly than :|:. We can write an
interpreter for RE into RegExp,
Enumerating
Instead of this writing a recogniser, let’s map a regular expression into a list of all the
strings that it matches. These lists might be infinite, but because Haskell uses lazy
evaluation, that’s not a problem.
interleave [] ys = ys
interleave (x:xs) ys = x : interleave ys xs
• The strings matching (re1 :*: re2) are of the form x++y where x matches
re1 and y matches re2. So, we need to generate all possible combinations of
elements from the two listings.
cartesian [] ys = []
cartesian (x:xs) ys
= [ x++y | y<-ys ] ‘interleave‘ cartesian xs ys
Supposing that the first argument is x:xs then we get all the combinations by
taking x with all choices from ys, and interleaving the results with all combi-
nations from xs and ys. To give an example,
plus :: RE -> RE
plus re = re :*: St re
• We can define a new constructor, adding this to RE:
data RE = ... |
Plus RE
What are the advantages and disadvantages of these two proposals?
Function plus. This option has the advantage of simplicity: we don’t need to ex-
tend any other functions once we have added this definition. The disadvan-
tage is that we are always committed to processing (e)+ as e followed by (e)*.
Constructor Plus. This option has the disadvantage that we have to extend all the
functions which deal with the RE type to include the new case of Plus re. The
advantage of this is that we then have the option to deal with this differently
from simply translating it out. For instance, we could make sure that a ‘plus’
was pretty printed as "(e)+" rather than "e(e)*", which would be the result
if we were to take the first option.
524 CHAPTER 19. DOMAIN-SPECIFIC LANGUAGES
simplify :: RE -> RE
simplify re = re
With this approach we build complex expressions, and then simplify them after-
wards. An alternative is never to build the complicated forms in the first place, and
we do this by defining smart constructors which do the simplification as they are
applied. For example, we can write
starC :: RE -> RE
starC (St re) = re
starC (Plus re) = re
starC re = (St re)
The DSLs we have looked at so far are all functional; in the next section we will see
that we can also add other aspects to a DSL, by making it monadic.
Exercises
You should use the functions already defined over RegExp to help you in doing
this.
19.2 Choose a suitable notation for writing down regular expressions as strings (e.g.
as used in Section 12.3) and then define functions to parse these strings into
RE, and to pretty-print elements of RE as strings:
Can you define QuickCheck properties that you would expect these functions
to satisfy?
526 CHAPTER 19. DOMAIN-SPECIFIC LANGUAGES
19.3 Define a recursive regular expression which will generate all palindromes built
from as and bs.
19.4 [Harder] Can you give a recursive regular expression which generates all (non-
recursive) regular expressions?
19.5 [Harder] Is there any limit to what else you can define using recursive regular
expressions: can you, for example, define all the strings which are strings of as
then bs, and then cs, each of the same length, as in aaabbbccc?
19.7 [Harder] Show how to extend the DSL to include these constructs:
let x=horse in
x ‘above‘ x
19.5. MONADIC DSLS 527
pic1:
horse
pic2:
horse
horse
The names will have to be added to the language somehow, and one option is to
build a data structure of the form
do
pic <- placeId horse (10,10)
pic2 <- positionId horse pic Center
position horse pic2 SW
The underlying implementation is the (Def a) monad, a state monad (as described
in Section 18.6) in which a unique identifier (an Id) is associated with each picture
528 CHAPTER 19. DOMAIN-SPECIFIC LANGUAGES
that has been positioned. The user interface to the monad is provided by the func-
tions:
Exercises
19.8 Define the state monad (Def a) which implements the information about
the position of pictures, and in particular give the instance declaration which
establishes that this is a monad.
19.9 Define the functions placeId, place, positionId and position over the
(Def a) monad.
19.10 Define a function or functions which allow you to extract the pictures from the
(Def a) monad so that they can be rendered somehow.
19.11 [Harder] Give an extension to regular expressions so that sub-components can
be named and the names used subsequently in the expression. An informal
example would be
((a|b)*:x)a<x>
robotic control language. In this section we’ll look at the example of QuickCheck,
which has at its heart a DSL to describe random or arbitrary data.
Data is generated in QuickCheck using Haskell’s random number generation.
The principal concept is that of a generator,
arbExpr 0 =
do int <- arbitrary
return (Lit int)
arbExpr n
| n>0 =
do
pick <- choose (0,2::Int)
case pick of
0 -> do
int <- arbitrary
return (Lit int)
1 -> do
left <- subExp
right <- subExp
return (Add left right)
2 -> do
left <- subExp
right <- subExp
return (Sub left right)
where
subExp = arbExpr (div n 2)
To generate a random Card we need both a random Int and a random String. We
use a do block to do this:
Note here that we’re using the notation to name results of computations, that is the
two random values that have been generated. We have given the types of the gen-
erators in comments, we don’t have to make this part of the program because the
type inference mechanism can detect their types from the way that the result is con-
structed.
What happens in the case when there is more than one alternative in the data
type definition? We give an example in Figure 19.5. In this case we pick between
19.6. DSLS FOR COMPUTATION: GENERATING DATA IN QUICKCHECK 531
the two cases by choosing an arbitrary Boolean, boo: in the True case we generate a
(Number int) and otherwise an (Email string).
Recursive generators
In a similar way we can declare an instance for a list type that we define ourselves:
While this approach works for the list type, we need to do something more so-
phisticated in the case of generating data for arbitrary recursive types, such as the
expressions used in the calculator case study.
Here we need to control the size of the values generated, so that the recursion
arising from generating expressions within expressions will terminate. We do this
by stating
where the function has type arbExpr :: Int -> Gen Expr, generating expres-
sions based on a size parameter. The sized function does the work of generation to
make sure that termination happens, assuming that we write a sensible definition
for arbExpr, as shown in Figure 19.6. The crucial point here is that the recursively
generated sub-expressions come from the subExpr generator, which is defined to
be arbExpr (div n 2) rather than arbExpr n.
frequency [(1,gen1),(2,gen2)]
We generate values from the gen1 and gen2 in the ratio 1:2, so 33% of the values will
come from gen1. We can use this to give a variant of the generator for expressions
presented in Figure 19.6:
where showMap is used to show the list of pairs (an exercise). To make a Show in-
stance we need to extract the String from the IO monad. We use the function
unsafePerformIO :: IO a -> a
which is defined in System.IO.Unsafe. This function should be used with care.
Finally we can say
arbExpr n = frequency
[(1, liftM Lit arbitrary),
(2, liftM2 Add subExp subExp),
(2, liftM2 Sub subExp subExp)]
where
subExp = arbExpr (div n 2)
prop_map f g xs =
map (f::Int->Int) (map (g::Int -> Int) xs) == map (g.f) xs
when we test it using QuickCheck:
which ‘lift’ an operation over values to the corresponding function over monadic
values. The values generated by this new generator for Exprs will be larger, as only
20% at any level will be literals (rather than 33% in our earlier definition).
QuickCheck has had a wide impact on programming practice in Haskell and
other languages. The original paper was published in 2000, and in 2010 it was awarded
an award for being the most influential paper presented at the International Confer-
ence on Functional Programming in 2000. The citation reads
This paper presented a very simple but powerful system for testing Haskell
programs that has had significant impact on the practice of debugging
534 CHAPTER 19. DOMAIN-SPECIFIC LANGUAGES
programs in Haskell. The paper describes a clever way to use type classes
and monads to automatically generate random test data. QuickCheck
has since become an extremely popular Haskell library that is widely
used by programmers, and has been incorporated into many under-
graduate courses in Haskell. The techniques described in the paper
have spawned a significant body of follow-on work in test case gener-
ation. They have also been adapted to other languages, leading to their
commercialisation for Erlang and C.
More information about using QuickCheck can be found in a number of places, in-
cluding in the original paper on QuickCheck, (Claessen and Hughes 2000), as well as
follow-up papers (Claessen and Hughes 2002; Claessen and Hughes 2003); in other
texts on programming, including (O’Sullivan, Stewart, and Goerzen 2008), and on-
line at URL. Note that there are a small number of differences between QuickCheck
1 and 2; we use version 2 in this text.
Exercises
19.12 Define a function which will give a pretty printed version of a sample from the
generator for expressions.
19.13 Define QuickCheck generators for the types used in the interactive version of
the calculator, as described in Section 18.3. Define properties that you would
expect (parts of) the calculator to satisfy, and test them using your generators
and QuickCheck.
modelling tools. In order to gain type safety, the implementation uses phantom
types to avoid constructing objects which are ill-typed from the point of view of the
domain (even if they are perfectly OK in Haskell). For ease of use numerical con-
stants and operators are overloaded – using Haskell classes – so that they apply not
only to numbers but also to numerical computations: this avoids introducing the
liftM functions we saw in the previous section. The full implementation uses fa-
cilities well beyond the Haskell 2010 standard, many of which are implemented in
GHC, and this is by no means unusual for larger-scale DSLs.
Naming in DSLs can be handled in many different ways. We saw already that
using a monadic approach gives us naming, but names in a monad aren’t given re-
cursive definitions. Looking at the example of a small logic circuit, note that the
output from the XOR gate is fed back into the gate after a delay.
input
XOR output
Reg
This chapter explores not the values which programs compute, but the way in which
those values are reached; we are interested here in program efficiency rather than
program correctness.
We begin our discussion by asking how we can measure complexity in general,
before asking how we measure the time and space behaviour of our functional pro-
grams. We work out the time complexity of a sequence of functions, leading up to
looking at various implementations of the Set abstype.
The space behaviour of lazy programs is complex: we show that some programs
use less space than we might predict, while others use more. This leads into a dis-
cussion of folding functions into lists, and we introduce the foldl’ function, which
folds from the left, and gives more space-efficient versions of folds of operators
which need their arguments – the strict operations. In contrast to this, foldr gives
better performance on lazy folds, in general.
In many algorithms, the naive implementation causes recomputation of parts
of the solution, and thus a poor performance. In the final section of the chapter
we show how to exploit lazy evaluation to give more efficient implementations, by
memoizing the partial results in a table.
537
538 CHAPTER 20. TIME AND SPACE BEHAVIOUR
f n = 2*n2 + 4*n + 13
grow, as n gets large? The function has three components:
• a constant 13,
• a term 4*n, and
• a term, 2*n2 . (Note that here we use the mathematical notation for powers,
n2 , rather than the Haskell notation, nˆ2.)
As the values of n become large, how do these components behave?
For ‘large’ values of n the square term is greater than the others, and so we say that
f is of order n2 , O(n2 ). In this case the square dominates for any n greater than or
equal to 3; we shall say exactly what is meant by ‘large’ when we make the definition
of order precise. As a rule of thumb we can say that order classifies how functions
behave when all but the fastest-growing components are removed, and constant
multipliers are ignored; the remainder of the section makes this precise, but this
explanation should be sufficient for understanding the remainder of the chapter.
The notation n2 is the usual way that mathematicians write down ‘the function
that takes n to n2 ’. This is the notation which is generally used in describing com-
plexity, and so we use it here. In a Haskell program to describe the function we would
either write \n -> nˆ2 or use the operator section (ˆ2).
In the remainder of this section we make the idea of order precise, before exam-
ining various examples and placing them on a scale for measuring complexity.
f n ∑ d*(g n)
The definition expresses the fact that when numbers are large enough (n∏m) the
value of f is no larger than a multiple of the function g, namely (d*).g.
For example, f above is O(n2 ) since, for n greater than or equal to 1,
A scale of measurement
We say that f ø g if f is O(g), but g is not O(f); we also use f ¥ g to mean that f
is O(g) and simultaneously g is O(f).
We now give a scale by which function complexity can be measured. Constants
which are O(n0 ) grow more slowly than linear – O(n1 ) – functions, which in turn
grow more slowly than quadratic functions of order O(n2 ). This continues through
the powers, and all the powers (nk ) are bounded by exponential functions, such as
2n .
Two other points ought to be added to the scale. The logarithm function, log, grows
more slowly than any positive power, and the product of the functions n and log n,
n(log n) fits between linear and quadratic, like this:
n0 ø log n ø n1 ø n(log n) ø n2 ø ...
Counting
Many of the arguments we make will involve counting. In this section we look at
some general examples which we will come across in examining the behaviour of
functions below.
Example
1. The first question we ask is – given a list, how many times can we bisect it, before
we cut it into pieces of length one? If the length is n, after the first cut, the length of
each half is n/2, and after p cuts, the length of each piece is n/(2p ). This number
will be smaller than or equal to one when
2. The second question concerns trees. A tree is called balanced if all its branches
are the same length. Suppose we have a balanced binary tree, whose branches are
of length b; how many nodes does the tree have? On the first level it has 1, on the
second 2, on the kth it has 2(k-1) , so over all b+1 levels it has
1 1
2 2
2
4
£(log2 n) in the size of the tree. If a tree is not balanced, the length of its longest
branch can be of the same order as the size of the tree itself; see Figure 20.1 for an
example.
3. Our final counting question concerns taking sums. If we are given one object
every day for n days, we have n at the end; if we are given n each day, we have n2 ;
what if we are given 1 on the first day, 2 on the second, and so on? What is the sum
of the list [1 .. n], in other words? Writing the list backwards, as well as forwards,
we have
1 + 2 + 3 + ... + (n-1) + n +
n + (n-1) + (n-2) + ... + 2 + 1
which makes it £(n2 ), or quadratic. In a similar way, the sum of the squares is
£(n3 ), and so on.
Exercises
f n = 2*n2 + 4*n + 13
is £(n2 ).
20.2 Give a table of the values of the functions n0 , log n, n1 , n(log n), n2 , n3 and
2n for the values
20.3 By giving the values of d, m and c (when necessary), show that the following
functions have the complexity indicated.
20.4 Show that nk ø 2n for all positive k. By taking logarithms of both sides, show
that log n ø nk for all positive k.
log ¥ ln ¥ log2
and in fact that logarithms to any base have the same rate of growth.
fib 0 = 0
fib 1 = 1
fib m = fib (m-2) + fib (m-1)
20.7 Show that ø is transitive – that is føg and gøh together imply that føh.
Show also that ¥ is an equivalence relation.
20.8 If f is O(g), show that any constant multiple of f is also of the same order. If
f1 and f2 are O(g), show that their sum and difference are also O(g). Are the
same results valid with £ replacing O?
f n = f1 n * f2 n
is O(n(k1+k2) ).
1 + 2 + 4 + ... + 2n = 2(n+1) - 1
1 + 2 + ... + n = n*(n+1) ‘div‘ 2
12 + 22 + ... + n2 = n*(n+1)*(2*n+1) ‘div‘ 6
13 + 23 + ... + n3 = (n*(n+1) ‘div‘ 2)2
542 CHAPTER 20. TIME AND SPACE BEHAVIOUR
• The time taken to compute a result is given by the number of steps in a calcu-
lation which uses lazy evaluation.
• The space necessary for the computation can be measured in two ways. First,
there is a lower limit on the amount of space we need for a calculation to com-
plete successfully. During calculation, the expression being calculated grows
and shrinks; obviously, we need enough space to hold the largest expression
built during the calculation. This is often called the residency of the compu-
tation, we shall call it the space complexity.
• We can also make a measure of the total space used by a computation, which
in some way reflects the total area of the calculation; it is of interest to imple-
menters of functional languages but for users (and for us) the first two are the
crucial measures.
Complexity measures
We measure the complexity of the function f by looking at the time and space com-
plexity as described above, as functions of the size of the inputs to f. The size of a
number is the number itself, while the size of a list is given by its length, and of a tree
by the number of nodes it contains. We now look at a series of examples.
Example
1. Let us start with the example of fac.
fac n
; n * fac (n-1)
; ...
; n * ((n-1) * ... * (2 * (1 * 1)) ...) (facMax)
; n * ((n-1) * ... * (2 * 1) ...)
; n * ((n-1) * ... * 2 ...)
20.2. THE COMPLEXITY OF CALCULATIONS 543
; ...
; n!
The calculation contains 2*n+1 steps, and the largest expression, (facMax), con-
tains n multiplication symbols. This makes the time and space complexity both
£(n1 ), or linear.
iSort [] = []
iSort (x:xs) = ins x (iSort xs)
ins x [] = [x]
ins x (y:ys)
| (x<=y) = x:y:ys
| otherwise = y:ins x ys
followed by the calculation of the n ins’s. What sort of behaviour does ins have?
Take the general example of
where we assume that [a1 ,...,an ] is sorted. There are three possibilities:
• In the best case, each ins will take one step, and the calculation will therefore
take a further n steps, making it O(n1 ) in this case.
• On the other hand, in the worst case, the first ins will take one step, the second
two, and so on. By our counting argument in Section 20.1 the calculation will
take O(n2 ) steps.
• In an average case, the ins’s will take a total of
steps, whose sum is again O(n2 ), by our observation in Section 20.1 about the
size of the sum 1+2...n.
We therefore see that in most cases the algorithm takes quadratic time, but in some
exceptional cases, when sorting an (almost) sorted list, the complexity is linear in
the length of the list. In all cases the space usage will also be linear.
3. Before looking at another sorting algorithm, we look at the time taken to join
together two lists, using ++.
qSort [] = []
qSort (x:xs) = qSort [z|z<-xs,z<=x] ++ [x] ++ qSort [z|z<-xs,z>x]
When the list is sorted and contains no duplicate elements, the calculation goes
thus:
Since the number of steps here is 1+2+. . . n, we have quadratic behaviour in this
sorted case. In the average case, we split thus
where the list has been bisected. Forming the two sublists will take O(n1 ) steps,
as will the joining together of the results. As we argued in Section 20.1, there can be
log2 n bisections before a list is reduced to one-element lists, so we have O(n1 ) steps
to perform O(log n) many times; this makes quicksort take O(n(log n)) steps,
20.2. THE COMPLEXITY OF CALCULATIONS 545
on average, although we saw that it can take quadratic steps in the worst (already
sorted!) case.1
The logarithmic behaviour here is characteristic of a ‘divide and conquer’ algo-
rithm: we split the problem into two smaller problems, solve these and then recom-
bine the results. The result is a comparatively efficient algorithm, which reaches its
base cases in O(log2 n) rather than O(n1 ) steps.
Exercises
20.11 Estimate the time complexity of the two reverse functions given here:
rev1 [] = []
rev1 (x:xs) = rev1 xs ++ [x]
and
rev2 = shunt []
shunt xs [] = xs
shunt xs (y:ys) = shunt (y:xs) ys
mult n 0 = 0
mult n m = mult n (m-1) + n
russ n 0 = 0
russ n m
| (m ‘mod‘ 2 == 0) = russ (n+n) (m ‘div‘ 2)
| otherwise = russ (n+n) (m ‘div‘ 2) + n
20.14 Show that the worst-case time behaviour of the merge sort function below is
O(n(log n)).
mSort xs
| (len < 2) = xs
1 The explanation we have given here depends upon us rearranging the order of the calculation steps;
this is legitimate if we observe that lazy evaluation of combinators is optimal, in the sense of taking fewest
steps to reach a result; any rearrangement can only give more steps to our calculation, so the bound of
n(log n) holds.
546 CHAPTER 20. TIME AND SPACE BEHAVIOUR
empty = []
memSet = member
inter xs ys = filter (member xs) ys
union = (++)
subSet xs ys = and (map (member ys) xs)
eqSet xs ys = subSet xs ys && subSet ys xs
makeSet = id
mapSet = map
We can also write an implementation based on the search trees of Section 16.7. We
now compare the time complexity of these implementations, and summarize the
results in the table which follows.
As we can see from the table, there is no clear ‘best’ or ‘worst’ choice; depending
upon the kind of set operation we intend to perform, different implementations
20.4. SPACE BEHAVIOUR 547
make more sense. This is one more reason for providing the abstract data type
boundary beneath which the implementation can be changed to suit the use to
which the sets are being put without any need to change the user programs. Us-
ing a variant of search trees which are ‘balanced’ so that all branches are of almost
the same length, it is possible to achieve linear complexity for subSet and inter on
average.
Exercises
20.15 Confirm the time complexities given in the table above for the two list imple-
mentations of sets.
20.16 Implement the operations subSet, inter, makeSet and mapSet for the search
tree implementation, and estimate the time complexity of your implementa-
tions.
20.17 Give an implementation of sets as lists without repetitions, and estimate the
time complexity of the functions in your implementation.
Lazy evaluation
Recall the explanation of lazy evaluation in Section 17.1, where we explained that
parts of results are printed as soon as possible. Once part of a result is printed, it
need no longer occupy any space. In estimating space complexity, we must be aware
of this.
Take the example of the lists [m .. n], defined thus
[m .. n]
| n>=m = m:[m+1 .. n]
| otherwise = []
Calculating [1 .. n] gives
[1 .. n]
?? n>=1
; 1:[1+1 .. n]
?? n>=2
; 1:[2 .. n]
; 1:2:[2+1 .. n]
; ...
; 1:2:3:...:n:[]
548 CHAPTER 20. TIME AND SPACE BEHAVIOUR
where we have underlined those parts of the result which can be output. To mea-
sure the space complexity we look at the non-underlined part, which is of constant
size, so the space complexity is O(n0 ). The calculation has approximately 2*n steps,
giving it linear time complexity, as expected.
exam1 = [1 .. n] ++ [1 .. n]
The time taken to calculate this will be O(n1 ), and the space used will be O(n0 ), but
we will have to calculate the expression [1 .. n] twice. Suppose instead that we
compute
Saving space?
As we saw in Section 20.2, the naive factorial function has O(n1 ) space complexity,
as it forms the expression
newFac n
; aFac n 1
; aFac (n-1) (1*n)
?? (n-1)==0 ; False
; aFac (n-2) (1*n*(n-1))
; ...
; aFac 0 (1*n*(n-1)*(n-2)*...*2*1)
; (1*n*(n-1)*(n-2)*...*2*1) (needVal)
so that the effect of this program is exactly the same: it still forms a large unevaluated
expression! The reason that the expression is unevaluated is that it is not clear that
its value is needed until the step (needVal).
How can we overcome this? We ought to make the intermediate values needed,
so that they are calculated earlier. We do this here by adding a test; another method
is given in Section 20.5.
aFac n p
| p==p = aFac (n-1) (p*n)
Now the calculation of the factorial of 4, say, is
aFac 4 1
; aFac (4-1) (1*4)
?? (4-1)==0 ; False
?? (1*4)==(1*4) ; True (eqTest)
; aFac (3-1) (4*3)
?? (3-1)==0 ; False
?? (4*3)==(4*3) ; True (eqTest)
; aFac (2-1) (12*2)
; ...
; aFac 0 (24*1)
; (24*1)
; 24
The lines (eqTest) show where the guard p==p is tested, and so where the inter-
mediate multiplications take place. From this we can conclude that this version has
better (constant) space behaviour.
550 CHAPTER 20. TIME AND SPACE BEHAVIOUR
Exercises
20.18 Estimate the space complexity of the function
where
20.19 Give an informal estimate of the complexity of the text processing functions
in Chapter 7.
Strictness
A function is strict in an argument if the result is undefined whenever an undefined
value is passed to this argument. For instance, (+) is strict in both arguments, while
(&&) is strict in its first only. Recall that it is defined by
True && x = x
False && x = False (andFalse)
The pattern match in the first argument forces it to be strict there, but equation
(andFalse) shows that it is possible to get an answer from (&&) when the second
argument is undef, so it is therefore not strict in the second argument.
If a function is not strict in an argument, we say that it is non-strict or lazy in
that argument.
foldr f st [] = st
foldr f st (x:xs) = f x (foldr f st xs)
20.5. FOLDING REVISITED 551
which we saw was of general application. Sorting a list, by insertion sort, was given
by
and indeed any primitive recursive definition over lists can be given by applying
foldr.
Writing the function applications as infix operations gives
and shows why the ‘r’ is added to the name: bracketing is to the right, with the
starting value st appearing to the right of the elements also. If f is lazy in its second
argument, we can see from (foldr) that given the head of the list, output may be
possible. For instance, map can be defined like this
foldr ((:).(+2)) [] [1 .. n]
; ((:).(+2)) 1 (foldr ((:).(+2)) [] [2 .. n])
; 1+2 : (foldr ((:).(+2)) [] [2 .. n])
; 3 : (foldr ((:).(+2)) [] [2 .. n])
; ...
As in Section 20.4, we see that the space complexity of this will be O(n0 ), since the
elements of the list will be output as they are calculated. What happens when we fold
a strict operator into a list? The definition of fac in Section 20.2 can be rewritten as
and we saw there that the effect was to give O(n1 ) space behaviour, since the mul-
tiplications in equation (foldr) cannot be performed until the whole expression
is formed, as they are bracketed to the right. We therefore define a function to fold
from the left.
which gives
foldl (*) 1 [1 .. n]
; foldl (*) (1*1) [2 .. n]
; ...
; foldl (*) (...((1*1)*2)*...*n) []
; (...((1*1)*2)*...*n)
As in Section 20.2, the difficulty is that foldl as we have defined it is not strict in its
second argument. Using the standard function seq
then strict f is a strict version of the function f which evaluates its argument x
before computing the result f x. We can therefore write a strict version of foldl,
called foldl’,
and this definition can be found in the Data.List module. Now, evaluating the
example again,
foldl’ (*) 1 [1 .. n]
; foldl’ (*) 1 [2 .. n]
; foldl’ (*) 2 [3 .. n]
; foldl’ (*) 6 [4 .. n]
; ...
Clearly, this evaluation is in constant space, O(n0 ). Can we draw any conclusions
from these examples?
ghc Main.hs
20.5. FOLDING REVISITED 553
accIA n m s
| n>m = s
| otherwise = accIA (n+1) m (n+s)
accIS n m s
| n>m = s
| otherwise = accIS (n+1) m $! (n+s)
and generates the executable file a.out, which we rename perfI.out, perfIA.out
and perfIS.out to distinsguish the three different variants of the main program.
In the module we see three definitions of functions to sum integer ranges: sumI,
a standard fold from the right, sumIA, a (lazy) fold from the left, and sumIS a strict
fold from the left.
As we suggested earlier, we would only expect the last of the three to have rea-
sonable behaviour, and indeed executing the compiled code for the first two gives
this error message:
The brackets +RTS ... -RTS are used to pass parameters to the Haskell runtime
system. We can increase the stack size and gather post-mortem information using
the flag -sstderr like this:
The report this produces is shown in Figure 20.3, which we explain now.
554 CHAPTER 20. TIME AND SPACE BEHAVIOUR
• The first block of information explains how much memory has been used by
the computation, and it is clear from this that the maximum space used – the
residency – is high.
• In the middle block we see the time devoted to various parts of the computa-
tion. INIT and EXIT explain the time to start and clean up, but the interesting
results are MUT the mutation time – that is time actually computing – and GC,
which is time dealing with storage: recycling information that is not used, and
copying information that is still in use. We can see from this that of the total
time, 89.6% is in GC, so we’re doing something wrong here.
The performance of perfIA.out is twice as bad (!), but for perfIS.out, as shown
in Figure 20.4, we see something much better. How do the two reports compare?
• The mutation time for the two computations is similar for the two: 0.22 sec-
onds for sumI and 0.16 for sumIA.
• On the other hand, the space behaviour is radically different. As we saw ear-
lier, GC time for sumI is almost two seconds, whereas for sumIA it is negligible.
These reports give a clear indication of where problems can occur, and GHC has
other facilities – such as heap profiling – to help users to see where space is sued in
a large system. The GHC documentation will tell you more.
20.5. FOLDING REVISITED 555
Designing folds
When we fold in a strict function, we will form a list-sized expression with foldr, so
it will always be worth using foldl’. This covers the examples of (+), (*) and so
forth.
We saw earlier that when map was defined using foldr we could begin to give
output before the whole of the list argument was constructed. If we use foldl’
instead, we will have to traverse the whole list before giving any output, since any
foldl’ computation follows the pattern
foldr or foldl’? The latter will give a constant-space version, but will examine the
entire list. Since (&&) is lazy in its second argument, we might not need to examine
the value returned from the remainder of the list. For instance,
This version uses constant space, and may not examine the whole list; foldr is
therefore the best choice.
Beside the examples of (+) and (*), there are many other examples where foldl’
is preferable, including:
• Reversing a list. To use foldr we have to add an element a to the end of a list,
x. The operation x++[a] is strict in x, while the ‘cons’ operation (:) is lazy in
its list argument.
• Converting a list of digits "7364" into a number is strict in both the conversion
of the front, 736 and the final character, ’4’.
Since foldl’ consumes an entire list before giving any output, it will be of no use
in defining functions to work over infinite lists or the partial lists we looked at while
writing interactive systems.
Exercises
20.20 Define the functions to reverse a list and to convert a digit list into a num-
ber using both foldr and foldl’ and compare their behaviour by means of
calculation.
20.21 Is it better to define insertion sort using foldr or foldl’? Justify your answer.
20.22 How are the results of foldr and foldl’ related? You may like to use the
functions reverse and flip in framing your answer.
20.23 What is the relationship between foldr and foldl’ when the function to be
folded is
fibP 3
= (y,x+y)
where
(x,y) = fibP 2
= (y1 ,x1 +y1 )
where
(x1 ,y1 ) = fibP 1
= (y2 ,x2 +y2 )
where
(x2 ,y2 ) = fibP 0
= (0,1)
= (1,1)
= (1,2)
= (2,3)
been used for the different occurrences of the local variables x and y; this is not
necessary but does make the different occurrences clearer.
As an alternative strategy, we can try to define the list of Fibonacci values, fibs,
directly. The values of the fib function given above now become values at particular
indices:
fibs :: [Integer]
fibs!!0 = 0
fibs!!1 = 1
fibs!!(n+2) = fibs!!n + fibs!!(n+1)
This gives a description of the list, but it is not executable in this form. The first two
lines tell us that fibs = 0 : 1 : rest, while the third equation tells us what the
rest is. The (n+2)nd element of fibs is the nth element of rest; similarly, the
(n+1)st element is the nth element of (tail fibs). We therefore have, for every n,
which says that each element is got by adding the corresponding elements of two
lists, that is
fibs ::[Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
a process network computing the Fibonacci numbers. This gives a linear time, con-
stant space algorithm for the problem, in contrast to the pair solution which is linear
in both time and space, since all the nested calls to fibP are built before any result
can be given.
Dynamic programming
The example in this section illustrates a general method of solving problems by
what is known as dynamic programming. Dynamic programming solutions work
by breaking a problem into subproblems but, as in the Fibonacci example, the sub-
problems will not be independent, in general. A naive solution therefore will contain
massive redundancy, which we remove by building a table of solutions to subprob-
lems.
The example we consider is to find the length of a maximal common subse-
quence of two lists – the subsequences need not have all their elements adjacent. In
the examples of
[2,1,4,5,2,3,5,2,4,3] [1,7,5,3,2]
the length of 4 is given by the subsequence [1,5,3,2]. This problem is not simply
a ‘toy’; a solution to this can be used to find the common lines in two files, which
20.6. AVOIDING RECOMPUTATION: MEMOIZATION 559
mLen xs [] = 0
mLen [] ys = 0
mLen (x:xs) (y:ys)
| x==y = 1 + mLen xs ys
| otherwise = max (mLen xs (y:ys)) (mLen (x:xs) ys)
maxLen :: Eq a => [a] -> [a] -> Int -> Int -> Int
maxLen xs ys 0 j = 0 (maxLen.1)
maxLen xs ys i 0 = 0 (maxLen.2)
maxLen xs ys i j
| xs!!(i-1) == ys!!(j-1)
= (maxLen xs ys (i-1) (j-1)) + 1 (maxLen.3)
| otherwise
= max (maxLen xs ys i (j-1)) (maxLen xs ys (i-1) j) (maxLen.4)
maxTab xs ys
= result
where
result = [0,0 .. ] : zipWith f [0 .. ] result
f i prev
= ans
where
ans = 0 : zipWith g [0 .. ] ans
g j v
| xs!!i == ys!!j = prev!!j + 1
| otherwise = max v (prev!!(j+1))
gives the basis of the Unix diff program, which is used, for instance, for comparing
different versions of programs stored in separate files.
The naive solution is given by mLen in Figure 20.6. The interesting part of the
definition is given by the third equation. In the case where the lists have equal first
elements, these elements must be in a maximal common subsequence, so we find
the overall solution by looking in the tails and adding one to the result. More prob-
lematic is the case in which the heads are distinct. We have the choice of excluding
either x or y; in this algorithm we try both possibilities and take the maximal result.
There, of course, is the source of the redundant computations – each of these may
well give rise to a computation of mLen xs ys. How are we to avoid this situation?
560 CHAPTER 20. TIME AND SPACE BEHAVIOUR
We shall store these results in a table, which will be represented by a list of lists. Once
a result appears in the table, we have no need to recompute it.
As an intermediate step, we rewrite the solution as maxLen which uses list index-
ing, so that
maxLen xs ys u v
is the longest common subsequence in the lists take u xs and take v ys. The
function is given in Figure 20.6, and the definition is a straightforward adaptation of
mLen.
Now we aim to define the table maxTab xs ys so that
(maxTab xs ys)!!u!!v = maxLen xs ys u v
This requirement is made specific by equations (maxLen.1) to (maxLen.4). The
base case is given by (maxLen.1), stating that
(maxTab xs ys)!!0!!v = 0
for all v. In other words,
(maxTab xs ys)!!0 = [0,0 .. ]
so,
result = [0,0 .. ] : ...
The equations (maxLen.2) to (maxLen.4) tell us how to define the list maxTab!!(i+1)
from the list maxTab!!i, and i, so we can define
maxTab xs ys = result
where
result = [0,0 .. ] : zipWith f [0 .. ] result
where f :: Integer -> [Integer] -> [Integer] is the function taking i and
the previous value, maxTab!!i, to maxTab!!(i+1). Now we have to define this lat-
ter, which appears in the solution as ans.
Equation (maxLen.2) tells us that it starts with 0, and g is the function taking
maxTab!!(i+1)!!j and j to maxTab!!(i+1)!!(j+1), where we are also able to
use the values of maxTab!!i, named by prev. Using these insights, the definition of
g is a straightforward transliteration of (maxLen.3) and (maxLen.4):
ans = 0 : zipWith g [0 .. ] ans
g j v
| xs!!i == ys!!j = prev!!j + 1
| otherwise = max v (prev!!(j+1))
The top-level result is given by calling
maxTab xs ys !! (length xs) !! (length ys)
and this is computed in linear time and space.
Haskell provides arrays which can be used to give a more efficient implementa-
tion of a number of algorithms, including this one here. Further details can be found
in the library module Array.hs and its documentation.
20.6. AVOIDING RECOMPUTATION: MEMOIZATION 561
Greedy algorithms
A greedy solution to a dynamic programming problem works by building up the op-
timal solution by making local choices of what appear to be the best solutions of
sub-problems. In the common subsequence problem, we can think of searching
along the two lists in a single sweep, looking successively for the first points of agree-
ment; we search all pairs of indices smaller than n before looking at n. In an example,
the greedy solution gives
1 2 3
2 4 1 2 3
which is not optimal: the subsequence [1,2,3] has been missed, since we make the
choice of 2 the first element, it is the first point of agreement. This local choice is not
part of an optimal global solution, but the algorithm gives reasonable performance.
In many situations, where local choices are always part of a global solution, a
greedy solution will work. Examples we have seen thus far include
Exercises
20.24 Give an implementation of the greedy solution to the maximal common sub-
sequence problem, and show that it behaves as explained above on the lists
[1,2,3] and [2,4,1,2,3] above.
20.25 Can you give an improvement of the maximal common subsequence solution
along the lines of fibP, returning a complex (finite) data structure as the result
of a function call, rather than simply one value?
20.26 Finding the ‘edit distance’ between two strings was first discussed in Section
14.5 where we gave a dynamic programming solution to the problem. Show
how you can give an efficient implementation of this algorithm using the tech-
niques of this section, and also how you give a greedy solution to the problem.
How do the two solutions compare?
20.27 Based on the examples of this section, provide a program which gives the dif-
ference between two files, matching the corresponding lines and giving the
562 CHAPTER 20. TIME AND SPACE BEHAVIOUR
output in a suitable form, such as a list of the pairs of matching line numbers
or a form copied from the Unix diff program.
Summary
In this chapter we have examined the efficiency of lazy functional programs. We
saw that we are able to analyse the time complexity of many of our more straightfor-
ward functions without too much difficulty. To analyse the space behaviour is more
difficult, but we have shown how the space consumption of lazy programs can be
estimated from our calculations.
The introduction of foldl brings the space issue into focus, and the distinc-
tion we made between strict and lazy functions allows us to analyse the different
behaviour of the two folds.
We concluded the discussion with an application of lazy infinite lists to memo-
izing results for reuse; the transition from naive to efficient was done in a systematic
way, which can be carried over to other application areas.
This chapter has provided an introduction to the study of functional program
behaviour; much more information – particularly about functional data structures
– can be found in Okasaki (1998).
Chapter 21
Conclusion
This book has covered the basics of functional programming in the lazy language
Haskell. It has shown how to craft programs, both by giving extensive examples as
each new aspect of the language was introduced, and also by giving a series of larger
case studies that run through the book.
which tests for membership of a list using the overloaded equality function.
563
564 CHAPTER 21. CONCLUSION
We can get strong evidence that this property holds by testing it for randomly
generated values of f and g using QuickCheck.
If we want to do some more work, we can prove this property from the defi-
nitions of map and composition. Proof provides a user with assurance about
how a program behaves on all arguments, in contrast to testing which can only
give direct information about its behaviour on a (hopefully) representative se-
lection of inputs.
• Pulling all this together, we can use these and other facilities of Haskell to build
implementations of domain-specific languages (DSLs) embedded in Haskell.
These DSLs allow users to express problems and models in a language appro-
priate to the domain, but at the same time to use all the power of Haskell when
necessary.
A text like this can only provide an introduction to a subject as rich and developed as
functional programming; the rest of this concluding chapter discusses other aspects
of the subject, as well as giving pointers to other sources on the Web and in books
and articles.
Further Haskell
The purpose of this text is to introduce functional programming ideas using the
Haskell language. It covers the important aspects of the language, but does not aim
to be complete. Among the topics omitted are data types with labelled fields, which
resemble records or structures in other languages; strictness annotations, which are
used to make data type constructors strict in some or all of their arguments; details
of the Read class and the numeric types and classes.
Further information about all these can be found in the Haskell language re-
port (Marlow 2010), and the ‘Gentle Introduction’ of (Hudak, Fasel, and Peterson
2000) also contains useful information about some of them, as well as providing an
overview of the language for an experienced functional programmer. Both of these,
as well as many other Haskell resources, can be found at the Haskell home page,
http://www.haskell.org/.
565
Where can you go to find out what to do next in Haskell? Again the haskell.org
site has many links, but three specific things you might like to look at are
• Real World Haskell (O’Sullivan, Stewart, and Goerzen 2008) which is available
in print and also online. It starts from the beginning, but goes at a faster pace
than we did here, and so covers a number of practical aspects of Haskell which
we weren’t able to do, such as the foreign function interface. So, it’s the perfect
follow-on read.
• Learn You a Haskell for Great Good! (Lipovača 2010) is a website and soon to
be a printed book which introduces Haskell for those who are familiar with
imperative programming. It’s written in a very approachable style, and com-
plements what we have covered here, as well as going into some topics – like
monads and zippers – in much more detail.
• Pearls of Functional Algorithm Design (Bird 2010) this delightful book looks
at a series of 30 problems, and solves them using the design by calculation
approach, which is ideally suited to writing functional programs in Haskell.
The text has discussed many of the most important functions in the standard pre-
lude but on the whole has avoided discussing the contents of the libraries in detail.
As we explained in Chapter 6, they are documented in Haddock, and this documen-
tation is available online for type- and name-based search in Hoogle. Moreover, the
packages in Hackage are themselves documented and have information available in
Hayoo.
www.haskellcraft.com
which lists all the links given here. The Haskell home page, Figure 21.1, is at
http://www.haskell.org/
566 CHAPTER 21. CONCLUSION
and that should be your first stop for everything to do with Haskell. As you can see
from the figure, the home page has links to learning resources, to documentation
about libraries and packages, and to the wider Haskell community.
The Haskell community includes online forums (IRC, mailing lists), news (Red-
dit), blogging (Planet Haskell) and help (Stack Overflow). The Haskell Communities
and Activities reports provide a biannual snapshot of what’s going on with Haskell:
the most recent edition is 77 (two-column, A4) pages long, and contains a wealth
of detail on who is doing what with Haskell. Subscribers to Planet Haskell or the
mailing lists also receive Haskell Weekly News, which summarises Haskell-related
software releases, blog entries and other news.
The Haskell language was named in honour of Haskell Brooks Curry. A short
biography and photograph of Curry can be found at
http://www-history.mcs.st-and.ac.uk/Biographies/Curry.html
best known and most widely used strict and strongly typed functional language, for
which Paulson (1996) provides an introduction. The latest member of the ML family
of languages, is F# (Smith 2009), which is distributed by Microsoft as a part of Visual
Studio 2010. Since is possible to model lazy evaluation within a strict language, and
Haskell provides facilities to make evaluation strict, the Haskell and ML schools of
programming are very close indeed.
A different style of functional programming, ‘point-free programming’, eschews
variables as much as possible: this was introduced in Backus (1978). Bird and de
Moor (1997) is a text that emphasizes the benefits of this style in supporting program
transformation and also advocates a ‘relational’ style of programming which extends
the functional.
LISP is the oldest established functional language, but it differs from Haskell and
SML in not being strongly typed. An excellent tutorial introduction to programming
in the Scheme dialect of LISP is given in Abelson, Sussman, and Sussman (1996).
Land of Lisp (Barski 2010) is a book, website and music video1 introducing Lisp by
developing a series of games.
Erlang is a concurrent, fault-tolerant, distributed language, based on a func-
tional programming core. Erlang (Armstrong 2007; Cesarini and Thompson 2009)
was developed within Ericsson, and as well as its use in telecoms applications, has
applications in the financial sector, and to high-availability distributed systems in
general.
Two surveys of applications of functional programming languages in large-scale
projects are Runciman and Wakeling (1995) and Hartel and Plasmeijer (1995b), and
there is also up-to-date information about this at the Haskell home page.
Over the last two decades, powerful techniques of implementation of especially
lazy functional languages have been developed. The twin texts (Peyton Jones 1987;
Peyton Jones and Lester 1992) describe the foundations of these in lucid detail.
http://journals.cambridge.org/jfp
and at the annual International Conference in Functional Programming,
http://www.icfpconference.org/
1 . . . simple but refined, guaranteed to blow your mind . . .
568 CHAPTER 21. CONCLUSION
• type systems for languages like Haskell will become more powerful and eso-
teric, and
• tool support, for instance giving feedback on the behaviour of lazy programs,
will come to maturity.
Not a bad set of predictions. The first is certainly true, with Haskell inter-operating
effectively with a range of other languages through its foreign function interface.
GCH continues to be a laboratory for type system research, with recent advances in
type-level programming taking it ever closer to dependently-typed languages such
as Agda,
http://wiki.portal.chalmers.se/agda/
Tool support for Haskell has also come of age, with many tools integrated into GHC,
and it becoming easier for others to integrate their work through the definition of an
API for the internals of GHC. What I had not predicted was the growth in the Haskell
developer community: fuelled particularly by Hackage and Cabal, making it easy to
share and to work collaboratively, there are now close to three thousand projects on
Hackage, giving the developer community a critical mass which was entirely lacking
a decade ago.2
What of the next few years? The big challenge for systems developers is the rise
of multicore chips: chips with thousands of processors are on the roadmap, and so
the question arises of how best to program them, or indeed how to program them at
all! Functional languages, because of their lack of side-effects, and clean models for
concurrency, make them ideal candidates for the next generation of general purpose
languages for multicore. The next few years will be fascinating, and of course, un-
predictable, but it will be a surprise if functional languages are not playing a much
more important role in robust software development in ten years time, with Haskell
central to this achievement.
2 It was also a surprise that Microsoft started to deploy a functional language as a part of their main
language suite in Visual Studio: a friend mailed me and said that his first thought it was an ‘April fool’
message, even though it came in June!
Appendix A
These equations state what the sum of squares is for a natural number argument. In
the first case it is a direct description; in the second it states that the sum to non-zero
n is got by finding the sum to n-1 and adding the square of n.
A typical imperative program might solve the problem thus
s = 0 ;
i = 0 ;
while i<n do
begin
i = i+1 ;
s = i*i + s ;
end
The sum is the final value of the variable s, which is changed repeatedly during pro-
gram execution, as is the ‘count’ variable, i. The effect of the program can only
569
570 APPENDIX A. FUNCTIONAL, IMPERATIVE AND OO PROGRAMMING
be seen by following the sequence of changes made to these variables by the com-
mands in the program, while the functional program can be read as a series of equa-
tions defining the sum of squares. This meaning is explicit in the functional pro-
gram, whereas the imperative program has an overall effect which is not obvious
from the program itself.
The link between these two approaches is given by a tail-recursive solution to
the problem in Haskell:
sumSquares n = ssAcc 0 0 n
ssAcc i s n
| i<n = ssAcc (i+1) ((i+1)^2+s) n
| otherwise = s
Here the three argument positions play the role of three variables – i, s and n –
whose values are changed on each call to the loop.
A more striking algorithm still is one which is completely explicit: ‘to find the
sum of squares, build the list of numbers 1 to n, square each of them, and sum the
result’. This program, which uses neither complex control flow, as does the impera-
tive example, nor recursion as seen in the function sumSquares, can be written in a
functional style, thus:
a2 - b2 = (a-b)(a+b)
In any particular case, the value of all three occurrences of a will be the same. In
exactly the same way, in
571
Program verification
Probably the most important difference between functional and imperative pro-
grams is logical. As well as being a program, a functional definition is a logical equa-
tion describing a property of the function. Functional programs are self-describing,
as it were. Using the definitions, other properties of the functions can be deduced.
To take a simple example, for all n>0, it is the case that
sumSquares n > 0
To start with,
sumSquares 1
= 1*1 + sumSquares 0
= 1*1 + 0
= 1
Now, n*n is positive, and if sumSquares (n-1) is positive, their sum, sumSquares
n, must be. This proof can be formalized using mathematical induction. The body
of the text contains numerous examples of proofs by induction over the structure of
data structures like lists and trees, as well as over numbers.
Program verification is possible for imperative programs as well, but imperative
programs are not self-describing in the way functional ones are. To describe the ef-
fect of an imperative program, like the ‘sum of squares’ program above, we need to
add to the program logical formulas or assertions which describe the state of the
program at various points in its execution. These methods are both more indirect
and more difficult, and verification seems very difficult indeed for ‘real’ languages
like Pascal and C. Another aspect of program verification is program transforma-
tion in which programs are transformed to other programs which have the same
effect but better performance, for example. Again, this is difficult for traditional im-
perative languages.
572 APPENDIX A. FUNCTIONAL, IMPERATIVE AND OO PROGRAMMING
head : value;
tail : list
end;
then we have the following correspondence, where the Haskell head and tail func-
tions give the head and tail of a list.
[] nil
head ys ysˆ.head
tail ys ysˆ.tail
(x:xs) cons(x,xs)
function cons(y:value;ys:list):list;
var xs:list;
begin
new(xs);
xsˆ.head := y;
xsˆ.tail := ys;
cons := xs
end;
Functions such as
sumList [] = 0
sumList (x:xs) = x + sumList xs
can then be transferred to Pascal in a straightforward way.
function sumList(xs:list):integer;
begin
if xs=nil
then sumList := 0
else sumList := xsˆ.head + sumList(xsˆ.tail)
end;
A second example is
doubleAll [] = []
doubleAll (x:xs) = (2*x) : doubleAll xs
where we use cons in the Pascal definition of the function
function doubleAll(xs:list):list;
begin
if xs=nil
then doubleAll := nil
else doubleAll := cons( 2*xsˆ.head , doubleAll(xsˆ.tail) )
end;
574 APPENDIX A. FUNCTIONAL, IMPERATIVE AND OO PROGRAMMING
function doubleAll(xs:list):list;
begin
if xs=nil
then doubleAll := nil
else doubleAll := cons( 2*head(xs) , doubleAll( tail(xs) ) )
end;
This is strong evidence that a functional approach can be useful even if we are writ-
ing in an imperative language: the functional language can be the high-level design
language for the imperative implementation. Making this separation can give us
substantial help in finding imperative programs – we can think about the design
and the lower level implementation separately, which makes each problem smaller,
simpler and therefore easier to solve.
Higher-order functions
Traditional imperative languages give little scope for higher-order programming;
Pascal, Java and C allow functions as arguments, so long as those functions are not
themselves higher-order, but has no facility for returning functions as results. In C++
it is possible to return objects which represent functions by overloading the func-
tion application operator! This underlies the genericity hailed in the C++ Standard
Template Library, which requires advanced features of the language to implement
functions like map and filter.
Control structures like if-then-else bear some resemblance to higher-order
functions, as they take commands, c1 , c2 etc. into other commands,
Polymorphism
Again, this aspect is poorly represented in many imperative languages; the best we
can do in Pascal, say, is to use a text editor to copy and modify the list processing
575
code from one type of lists for use with another. Of course, we then run the risk that
the different versions of the programs are not modified in step, unless we are very
careful to keep track of modifications, and so on.
Polymorphism in Haskell is what is commonly known as generic polymorphism:
the same ‘generic’ code works over a whole collection of types. A simple example is
the function which reverses the elements in a list.
Haskell classes support what is known as ‘ad hoc’ polymorphism, or in object-
oriented terminology simply ‘polymorphism’, in which different programs imple-
ment the same operation over different types. An example of this is the Eq class of
types carrying an equality operation: the way in which equality is checked is com-
pletely different at different types. Another way of viewing classes is as interfaces
which different types can implement in different ways; in this way they resemble
the interfaces of object-oriented languages like Java.
As is argued in the text, polymorphism is one of the mechanisms which helps to
make programs reusable in Haskell; it remains to be seen whether this will also be
true of advanced imperative languages.
List comprehensions
List comprehensions provide a convenient notation for iteration along lists: the
analogue of a for loop, which can be used to run through the indices of an array.
For instance, to sum all pairs of elements of xs and ys, we write
The order of the iteration is for a value a from the list xs to be fixed and then for b
to run through the possible values from ys; this is then repeated with the next value
from xs, until the list is exhausted. Just the same happens for a nested for loop
for i:=0 to xLen-1 do
for j:=0 to yLen-1 do (twoFor)
write( x[i]+y[j] )
where we fix a value for i while running through all values for j.
In the for loop, we have to run through the indices; a list generator runs through
the values directly. The indices of the list xs are given by
[0 .. length xs - 1]
and so a Haskell analogue of (twoFor) can be written thus:
[ xs!!i + ys!!j | i <- [0 .. length xs - 1] ,
j <- [0 .. length ys - 1] ]
if we so wish.
Lazy evaluation
Lazy evaluation and imperative languages do not mix well. In Pascal, for instance,
we can write the function definition
function succ(x : integer):integer;
begin
y := y+1;
succ := x+1
end;
This function adds one to its argument, but also has the side-effect of increasing y
by one. If we evaluate f(y,succ(z)) we cannot predict the effect it will have.
• If f evaluates its second argument first, y will be increased before being passed
to f;
• on the other hand, if f needs its first argument first (and perhaps its second
argument not at all), the value passed to f will not be increased, even if it is
increased before the function call terminates.
In general, it will not be possible to predict the behaviour of even the simplest pro-
grams. Since evaluating an expression can cause a change of the state, the order
of expression evaluation determines the overall effect of a program, and so a lazy
implementation can behave differently (in unforeseen ways) from the norm.
ing a supply of random numbers; because of lazy evaluation, these numbers will
only be generated on demand.
If we were to implement this imperatively, we would probably keep in a variable
the last random number generated, and at each request for a number we would
update this store. We can see the infinite list as supplying all the values that the
variable will take as a single structure; we therefore do not need to keep the state,
and hence have an abstraction from the imperative view.
We have seen in Section 18.5 that there has been recent important work on inte-
grating side-effecting programs into a functional system by a monadic approach.
Conclusion
Clearly there are parallels between the functional and the imperative, as well as clear
differences. The functional view of a system is often higher-level, and so even if we
ultimately aim for an imperative solution, a functional design or prototype can be
most useful.
We have seen that monads can be used to give an interface to imperative fea-
tures within a functional framework. Many of the Haskell implementations offer
these facilities, and so give a method of uniting the best features of two important
programming paradigms without compromising the purity of the language. Other
languages, including Standard ML (Milner, Tofte, and Harper 1990) and F# (Smith
2009), combine the functional and the imperative, but these systems tend to lose
their pure functional properties in the process.
It is interesting to see the influence of ideas from modern functional program-
ming languages in the design of Java extensions. One of the main drawbacks of Java
for a long time was that it lacked generic polymorphism. The current mechanism for
generics in the Java standard owes its inspiration and much of its detail to Haskell
polymorphism.
578 APPENDIX A. FUNCTIONAL, IMPERATIVE AND OO PROGRAMMING
Appendix B
Glossary
We include this glossary to give a quick reference to the most widely used terminol-
ogy in the book. Words appearing in bold in the descriptions have their own entries.
Further references and examples are to be found by consulting the index.
579
580 APPENDIX B. GLOSSARY
the definitions which are contained in a with the left-hand side of the equation if
script as well as the built-in definitions. that particular clause is chosen during
evaluation. The clause chosen is the
Cancellation The rule for finding the
first whose guard evaluates to True.
type of a partial application.
Conformal pattern match An
Character A single letter, such as ’s’ equation in which a pattern appears on
or ’\t’, the tab character. They form the left-hand side of an equation, as in
the Char type.
(x,y) = ....
Class A collection of types. A class is
defined by specifying a signature; a type Constructor An algebraic type is
is made an instance of the class by specified by its constructors, which are
supplying an implementation of the the functions which build elements of
definitions of the signature over the the algebraic type.
type. In the example in the entry for
algebraic types, elements of the type are
Clause A clause is one of the
constructed using Leaf and Node; the
alternatives making up a conditional
elements are Leaf n where n::Int and
equation. A clause consists of a guard
Node s t where s and t are trees.
followed by an expression. When
evaluating a function application, the Context The hypotheses which appear
first clause whose guard evaluates to before => in type and class declarations.
True is chosen. A context M a means that the type a
must belong to the class M for the
Combinator Another name for a
function or class definition to apply. For
function.
instance, to apply a function of type
Comment Part of a script which plays Eq a => [a] -> a -> Bool
no computational role; it is there for the
reader to read and observe. Comments to a list and object, these must come
are specified in two ways: the part of the from types over which equality is
line to the right is made a comment by defined.
the symbol –; a comment of arbitrary Curried function A function of at least
length is enclosed by {- and -}. two arguments which takes its
arguments one at a time, so having the
Complexity A measurement of the
type
time or space behaviour of a function.
t1 -> t2 -> ... -> t
Composition The combination of two
functions by passing the output of one in contrast to the uncurried version
to the input of the other. (t1,t2,...) -> t
Concatenate To put together a The name is in honour of Haskell B.
number of lists into a single list. Curry, after whom the Haskell language
Conditional equation A conditional is also named.
equation consists of a left-hand side Declaration A definition can be
followed by a number of clauses. Each accompanied by a statement of the type
clause consists of a guard followed by of the object defined; these are often
an expression which is to be equated called type declarations.
581
Lazy evaluation The sort of expression and »=. Informally, a monad can be
evaluation in Haskell. In a function seen as performing some sorts of action
application only those arguments before returning an object. The two
whose values are needed will be monad functions respectively return a
evaluated, and moreover, only the parts value without any action, and sequence
of structures which are needed will be two monadic operations.
examined. Monomorphic A type is
Linear complexity Order 1, O(n1 ), monomorphic if it is not polymorphic.
behaviour. Most general type The most general
Lists A list consists of a collection of type of an expression is the type t with
elements of a particular type, given in the property that every other type for
some order, potentially containing a the expression is an instance of t.
particular item more than once. The list Mutual recursion Two definitions,
[2,1,3,2] is of type [Int], for each of which depends upon the other.
example.
Name A definition associates a name
Literal Something that is ‘literally’ a or identifier with a value. Names of
value: it needs no evaluation. Examples classes, constructors and types must
include 34, [23] and "string". begin with capital letters; names of
Local definitions The definitions values, variables and type variables
appearing in a where clause or a let begin with small letters. After the first
expression. Their scope is the equation letter, any letter, digit, ‘’’ or ‘_’ can be
or expression to which the clause or let used.
is attached. Natural numbers The non-negative
whole numbers: 0, 1, 2, . . . .
Map To apply an operation to every
element of a list. Offside rule The way in which the end
of a part of a definition is expressed
Mathematical induction A method of
using the layout of a script, rather than
proof for statements of the form ‘for all
an explicit symbol for the end.
natural numbers n, the statement P(n)
holds’. Operation Another name for
The proof is in two parts: the base function.
case, at zero, and the induction step, at Operator A function which is written
which P(n) is proved on the in infix form, between its arguments.
assumption that P(n-1) holds. The function f is made infix thus: ‘f‘.
Memoization Keeping the value of a Operator section A partially applied
sub-computation (in a list, say) so that it operator.
can be reused rather than recomputed,
Output When a function is applied to
when it is needed.
one or more inputs, the resulting value
Module Another name for a script; is called the output, or result. Applying
used particularly when more than one the ‘square’ function to (-2) gives the
script is used to build a program. output 4, for example.
Monad A monad consists of a type Overloading The use of the same
with (at least) two functions, return name to mean two (or more) different
584 APPENDIX B. GLOSSARY
Haskell operators
The operators in the Haskell prelude are listed below in decreasing order of binding
power: see Section 3.7 for a discussion of associativity and binding power.
9 >.>
5 >*>
The restrictions on names of operators, which are formed using the characters
587
588 APPENDIX C. HASKELL OPERATORS
meaning in certain circumstances – the obvious advice here is not to use them. Fi-
nally, certain combinations of symbols are reserved, and cannot be used: .. : ::
=> = @ \ | ˆ <- ->.
To change the associativity or binding power of an operator, &&& say, we make a
declaration like
infixl 7 &&&
which states that &&& has binding power 7, and is a left associative operator. We can
also declare operators as non-associative (infix) and right associative (infixr).
Omitting the binding power gives a default of 9. These declarations can also be used
for back-quoted function names, as in
infix 0 ‘poodle‘
Appendix D
Haskell practicalities
It’s not difficult to get going using Haskell, and most of the relevant information is
easily accessible from the haskell.org page. This appendix points you in the right
direction.
Implementations
Implementations of Haskell have been built at various sites around the world. This
text uses GHCi, an interactive front-end to the Glasgow Haskell Compiler (GHC).
GHCi provides much of the functionality of the Hugs interpreter, which was devel-
oped in a joint effort by staff at the Universities of Nottingham in the UK and Yale in
the USA. The first compilers for Haskell were developed at the University of Glasgow,
UK, and Chalmers Technical University, Göteborg, Sweden. More recent develop-
ments have taken place elsewhere, including at York and Utrecht. An up-to-date list
of implementations and their status can be found at
http://www.haskell.org/haskellwiki/Implementations
In this book we have used the Haskell Platform as our foundation. This is docu-
mented at
http://hackage.haskell.org/platform/
from where it can also be downloaded. Installation instructions for Windows, Mac
OS X and Linux are listed on the relevant downloads page.
www.haskellcraft.com
589
590 APPENDIX D. HASKELL PRACTICALITIES
Using GHCi
An overview of the main commands of GHCi can be found in Figure 2.4, page 32,
and full details of other aspects of GHCi are in the online documentation for GHC.
http://www.haskell.org/haskellwiki/Haskell_mode_for_Emacs
Not everyone gets on with emacs, and vim is an alternative for many, with its mode
available from
http://projects.haskell.org/haskellmode-vim/
Other editors include Yi, a text editor written in Haskell and extensible in Haskell.
http://www.haskell.org/haskellwiki/Yi
and an overview of all those available is at
http://www.haskell.org/haskellwiki/Editors
Appendix E
GHCi errors
This appendix examines some of the more common programming errors in Haskell,
and shows the error messages to which they give rise in GHCi.
The programs we write all too often contain errors. On encountering an error,
the system either halts, and gives an error message, or continues, but gives a warn-
ing message to tell us that something unusual has happened, which might signal
that we have made an error. In this appendix, we look at a selection of the messages
output by GHCi; we have chosen the messages which are both common and require
some explanation; messages like
Syntax errors
A Haskell system attempts to match the input we give to the syntax of the language.
Commonly, when something goes wrong, we type something unexpected.
fun x
fun 2 = 34
591
592 APPENDIX E. GHCI ERRORS
The problem here is that the system tries to understand fun and x and these
are not (yet) defined.
fun 2 = 34
fun x
fun x = x+1
where
type MyInt = Int
is signalled by
• The syntax of patterns is more restricted than the full expression syntax, and
so we get error messages like
Errors.hs:6:5:
Conflicting definitions for ‘x’
Bound at: Errors.hs:6:5
Errors.hs:6:7
In the definition of ‘fun’
when we use the same variable more than once within a pattern, as in the
definition
<interactive>:1:18:
lexical error in string/character literal at character ’\’’
593
• Not every string can be used as a name; some words in Haskell are keywords
or reserved identifiers, and will give an error if used as an identifier. The key-
words are
do (x,y) = x+1
As you can see from the two examples, the error message in a case like this
reflects the meaning of the keyword.
• The special identifiers as, qualified and hiding have special meanings in
certain contexts but can be used as ordinary identifiers.
• The final restriction on names is that names of constructors and types must
begin with a capital letter; nothing else can do so, and hence we get error mes-
sages like
Type errors
In this section we look at various different type errors that we can provoke in GHCi.
• As we have seen in the body of the text, the main type error we meet is exem-
plified by the response to typing ’c’ && True to the GHCi prompt:
True + 4
This comes from the class mechanism: the system attempts to make Bool an
instance of the class Num of numeric types over which ‘+’ is defined. The error
results since there is no such instance declaration making Bool belong to the
class Num.
• As we said before, we can get type errors from syntax errors. For example,
writing abs -2 instead of abs (-2) gives the error message
because it is parsed as 2 subtracted from abs::a->a, and the operator ‘-’ ex-
pects something in the class Num, rather than a function of type a->a. Other
common type errors come from confusing the roles of ‘:’ and ‘++’ as in 2++[2]
and [2]:[2].
• We always give type declarations for our definitions; one advantage of this is to
spot when our definition does not conform to its declared type. For example,
Without the type declaration the definition would be accepted, only to give an
error (presumably) when it is used.
595
• A definition like
asc x y
| x <= y = x y
The problem here is that x is compared with y in the guard (type a), but applied
to y in the body (type a->t): the unification required here produces an infinite
type, which is not allowed.
Program errors
Once we have written a syntactically and type correct script, and asked for the value
of an expression which is itself acceptable, other errors can be produced during the
evaluation of the expression.
• The first class of errors comes from missing cases in definitions. If we have
written a definition like
bat [] = 45
which shows the point at which evaluation can go no further, since there is no
case in the definition of bat to cover a non-empty list. Similar errors come
from built-in functions, such as head.
• Other errors happen because an arithmetical constraint has been broken.
These include an out-of-range list index, division by zero, using a fraction
where an integer is expected and floating-point calculations which go out of
range; the error messages all have the same form. For example, suppose that
we evaluate
3 ‘div‘ 0
then the error is
[a,b] = [1 .. 10]
this will fail with the message
Module errors
The module and import statements can provoke a variety of error messages: files
may not be present, or may contain errors; names may be included more than once,
or an alias on inclusion may cause a name clash. The error messages for these and
other errors are self-explanatory.
System messages
In response to some commands and interrupts, the system generates messages, in-
cluding
• ˆC ... Interrupted
signalling the interruption of the current task by typing Ctrl-C.
• The message
Project ideas
In this appendix we give some ideas for extended Haskell projects, building on what
we have covered here. Most of the projects can be implemented using what you have
learned in this text, but many would gain from using libraries on the Hackage site.
The projects are also discussed in more detail in the online supplement to the text,
which appears at www.haskellcraft.com.
Sudoku
A first puzzle here is to devise Sudoko problems, which can then be printed and
solved by hand. How do you find problems which fit the various levels of difficulty?
A second problem is to solve a given problem, as appears in many newspapers
and online. As well as thinking of the algorithm which finds a solution efficiently,
you’ll need to think about how to input the problem and how to present the results.
Finally, you could check whether the solution you find is unique: if not, can you enu-
merate or count the number of solutions? Conversely, are any seemingly consistent
problems in fact unsolvable?
A third problem is to provide assistance to a human solver: can you give hints
597
598 APPENDIX F. PROJECT IDEAS
how they might make the next move when their solution has got to a certain point.
Again you’ll need to think about just how the interaction will take place. This could
be part of an online solution assistant, or stand alone.
Minesweeper
The minesweeper program requires the player to uncover mines in a minefield with-
out setting any of them off, when the player is given the count of mines on squares
adjacent to each uncovered square. Many games are available online to provide ex-
amples.
One problem is to re-implement one of these interactive games: it could use "text
graphics", specifying the square to uncover by giving its coordinates, or could work
interactively in a graphics system or browser.
Alternatively, you could implement an algorithm to play the game automatically:
given a particular configuration, which is the optimal move to make next? How
much information do you need to know to make this decision? Can you solve the
problem in a deterministic way, or will your solution be probabilistic?
Web graphics
The web is moving towards a new standard, HTML5, which will directly render Scal-
able Vector Graphics (SVG) (SVG 2010) in web browsers. We have used SVG as a way
of rendering our pictures, but in doing this we have barely scratched the surface of
what is possible. The gtk package renders SVG using the Cairo system, but the aim
of the projects discussed here is to use standard web technology to do the ‘heavy
lifting’ of rendering.
Representing SVG
In this project you should devise a representation for a subset of SVG within Haskell.
You might like to look at what has been done in gtk, and also at the different ways
that XML is represented in HaXml and other Haskell libraries.
599
Rendering SVG
SVG can be rendered in place using a modern browser – in the case of Internet Ex-
plorer from version 9 upwards. Suppose that you want to generate SVG in a program,
and have that data rendered in a browser: one mechanism for this is to run a local
web-server in which the page is created, and then served to a client on the local
machine. Using the HTTP and Web libraries for Haskell, build this local web server.
Using the web forms standard in HTML5, you can build richer interactions, particu-
larly using a local web server running in Haskell. In particular, you should be able to
build a browser-embedded version of the calculator program, using Haskell to cal-
culate the value of the input expression, and performing the interaction using a web
form.
Alternatively, it is possible to build interactivity using JavaScript, either “raw” or
through the jQuery library.
Logic
Logic is a branch of mathematics closely linked to computer science (Huth and Ryan
2004), and implementing various logical procedures is a great way to understand
precisely how logic works.
Truth tables
The first exercise is to implement truth tables for formulas of propositional logic. We
can use these to decide whether a formula is a tautology (true in all interpretations)
or satisfiable (true in at least one interpretation). The output could just be this clas-
sification, or it could be the full truth table, showing the value of the formula under
each interpretation.
SAT solving
The practicalities of deciding whether or not a formula is satisfiable, the SAT solving
problem, is an area where huge efforts are made to find efficient algorithms to solve
the problem. How can these algorithms be implemented in Haskell?
Tableaux
Semantic tableaux give a decision procedure not just for propositional logic, but also
for temporal and other modal logics. A tableau method is a constructive mechanism,
which will find all the interpretations satisfying a particular formula. This project is
to implement a tableau procedure for propositional logic; can also look at tableaux
for the temporal logics in (Huth and Ryan 2004).
600 APPENDIX F. PROJECT IDEAS
Proof
Voting systems
Depending on where you live, you will decide the form of your government in differ-
ent ways. The simplest mechanism is perhaps first past the post, where the person
gaining the single largest number of votes is elected, but other systems include ways
of getting a more proportional outcome than this.
How is it possible to explain these various mechanisms to a naïve voter? The aim
of this exercise is to illustrate the effects of different systems, based on a simulated
vote.
Voting data
It should be possible to get actual voting data from particular elections, and to use
(processed?) variants of those data in different voting scenarios. Alternatively you
should generate random data according to a scenario specified by a user. This could
be done from scratch, or alternatively you could use the data generation facilities of
QuickCheck to build a sample.
Visualisation
Tactical voting
What is the best way to get the result that you want? On the basis of historical data
and the particular system, analyse the options for a voter who wants a particular
outcome to happen. For instance, what is the best option for your second vote in an
AV system?
Finite-state machines
One of the fundamental abstractions in computer science is the finite-state machine
(FSM) (Aho, Lam, Sethi, and Ullman 2006). We saw earlier that we can write recog-
nisers for regular expressions, but more efficient implementations are given by de-
riving NFAs from regular expressions, and then (minimal) DFAs from those NFAs.
601
Conversion chain
Inferring machines
Visualisation
Domain-specific languages
One theme of this book has been domain-specific languages, and as a part of some
of these projects you could build a domain-specific language. Examples include
• A language for describing games has been defined by Conway (Conway 2002;
Berlekamp, Conway, and Guy 2001): look at how you can build a DSL for these
games; you could also look at a language for describing strategies to play these
games.
• A language for describing different voting systems: your simulations and vi-
sualisations could then work with an arbitrary voting system, as described in
the language.
• We saw in the body of the text that it is possible to write a simple DSL for
patterns, namely regular expressions. Look at ways that this can be extended
to make it more expressible, and also at the possibility of defining a DSL to
describe different kinds of finite state machines.
These are just a few ideas of the kind of DSL that you could build: a general project
is to use Haskell for building DSLs in a domain of your choice.
602 APPENDIX F. PROJECT IDEAS
Bibliography
Abelson, H., G. J. Sussman, and J. Sussman (1996). The Structure and Interpreta-
tion of Computer Programs (Second ed.). MIT Press.
Aho, A. V., M. S. Lam, R. Sethi, and J. D. Ullman (2006). Compilers: Principles,
Techniques, and Tools (Second ed.). Addison Wesley.
Appel, A. (1993). A critique of Standard ML. Journal of Functional Programming 3.
Armstrong, J. (2007). Programming Erlang: Software for a Concurrent World. The
Pragmatic Bookshelf.
Augustsson, L., H. Mansell, and G. Sittampalam (2008). Paradise: a two-stage DSL
embedded in Haskell. In ICFP ’08, pp. 225–228. ACM.
Backus, J. (1978). Can programming be liberated from the Von Neumann style?
Communications of the ACM 21(8).
Barski, C. (2010). Land of Lisp: Learn to Program in Lisp, One Game at a Time!
Nostarch Press.
Berlekamp, E. R., J. H. Conway, and R. K. Guy (2001). Winning Ways for Your Math-
ematical Plays, Volume 1. A K Peters/CRC Press.
Bird, R. (2010). Pearls of Functional Algorithm Design. Cambridge University
Press.
Bird, R. and O. de Moor (1997). Algebra of Programming. Prentice-Hall.
Bjesse, P., K. Claessen, M. Sheeran, and S. Singh (1998). Lava: hardware design in
Haskell. In ICFP ’98, pp. 174–184. ACM.
Cabal (2010). The Haskell Cabal. Available at http://www.haskell.org/cabal/.
Cesarini, F. and S. Thompson (2009). Erlang Programming: A Concurrent Ap-
proach to Software Development. O’Reilly.
Claessen, K. and J. Hughes (2000). QuickCheck: a lightweight tool for random
testing of Haskell programs. In ICFP ’00, pp. 268–279. ACM.
Claessen, K. and J. Hughes (2002). Testing monadic code with QuickCheck. In
Haskell ’02, pp. 65–77. ACM.
Claessen, K. and J. Hughes (2003). Specification-based testing with QuickCheck.
In J. Gibbons and O. de Moor (Eds.), The Fun of Programming. Palgrave.
Conway, J. H. (2002). On Numbers and Games (Second ed.). A K Peters/CRC Press.
603
604 BIBLIOGRAPHY