Haskell PDF
Haskell PDF
The Craft of
Functional
Proaramming
Second edition
Simon Thompson
Addison-Wesley
and Rory
Contents
Preface xii
vii
viii Contents
10 Functions as values
10.1 Function-level definitions
10.2 Function composition
10.3 Functions as values and results
10.4 Partial application
10.5 Revisiting the P i c t u r e example
10.6 Further examples
10.7 Currying and uncurrying
10.8 Example: creating an index
10.9 Verification and general functions
11 Program development
11.1 The development cycle
11.2 Development in practice
13 Checking types
13.1 Monomorphic type checking
13.2 Polymorphic type checking
13.3 Type checking and classes
14 Algebraic types
14.1 lntroducing algebraic types
14.2 Recursive algebraic types
14.3 Polymorphic algebraic types
x Contents
17 Lazy programming
17.1 Lazy evaluation
17.2 Calculation rules and lazy evaluation
17.3 List comprehensions revisited
17.4 Data-directed programming
17.5 Case study: parsing expressions
17.6 Infinite lists
17.7 Why infinite lists?
17.8 Case study: simulation
17.9 Proof revisited
20 Conclusion
Appendices
B Glossary
C Haskell operators
D Understanding programs
E lmplementations of Haskell
F Hugs errors
Bibliography
Index
Preface
Functional programming offers a high-level view of programnii ng, giving its users a
variety of features which help them to build elegant yet powerful and general libraries of
functions. Central to functional programming is the idea of a function. which computes
a result that depends on the values of its inputs.
An example of the power and generality of the language is the map function, which
is used to transform every elenlent of a list of objects in a specified way. For example.
map can be used to double all the numbers in a sequence of numbers or to invert the
colours in each picture appearing in a list of pictures.
The elegance of functional programming is a consequence of the way that functions
are defined: an equation is used to say what the value of a function is o n an arbitrary
input. A simple illustration is the function addDouble which adds two integers and
doubles their sum. Its definition is
the equations which define the functions involved in the expression are used. so
This ic how a computer would work out the value of the expression, but it i \ also
possible to do exactly the same calculation using pencil and paper, mahing transparent
the implementation mechanism.
It i \ also possible to diccu\s 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. This idea
of proof is much more tractable than those for traditional imperative and object-oriented
(00)languages.
This text uses the programming language Haskell, which has freely available compilers
and interpreters for most types of computer system. Used here is the Hugs interpreter
which provides an ideal platform for the learner, with its fast compile cycle, siniplc
interface and free availability for Windows, Unix and Macintosh systemc.
Haskell began life in the late 1980s as an intended standard language for lazy
functional programming, and since then it has gone through various changes and
xiv Preface
There is a tension in writing about a programming language: one wants to introduce all
the aspects of the language as early as possible, yet not to over-burden the reader with
too much at once. The firs1 edition of the text introduced ideas 'from the bottom up',
which meant that it took more than a hundred pages before any substantial example
could be discussed.
The second edition takes a different approach: a case study of 'pictures' introduces
a number of crucial ideas informally in the first chapter, revisiting then1 as the text
proceeds. Also, Haskell has a substantial library of built-in functions, particularly over
lists, and this edition exploits this, encouraging readers to use these functions before
seeing the details of their definitions. This allows readers to progress more quickly.
and also accords with practice: most real programs are built using substantial libraries
of pre-existing code, and it is therefore valuable experience to work in this way from
the start. A section containing details of the other changes in the second edition can be
found later in this preface.
Other distinctive features of the approach in the book include the following.
The text gives a thorough treatment of reasoning about functional programs. be-
ginning with reasoning about list-manipulating functions. Thesc are chosen in
preference to functions over the natural numbers for two reasons: the rcsults one
can prove for lists seem substantially more realistic, and also the structural induction
principle for lists seenis to be more acceptable to students.
The P i c t u r e case study is introduced in Chapter I and revisited throughout the tcxt:
this means that readers see different ways of programming the same function, and so
get a chance to reflect on and compare different designs.
Function design - t o be done before starting to code - is also emphasized explicitly
in Chapters 4 and 1 1 .
There i \ an emphasis on Haskell as a practical programming language, with an early
introduction of nlodules, as well as a thorough examination of the do notation for
110 and other monad-bawd applications.
Types play a prominent role in the text. Every function or object defined has its typc
introduced at the samc time as its definition. Not only does this provide 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 definition, 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 picture
example noted above, an interactive calculator program, a coding and decoding
system based on Huffinan codes and a small queue simulation package. These are
used to introduce various new ideas and also to show how existing techniques work
together.
Support materials on Haskell, including a substantial number of Web links, are
included in the concluding chapter. Various appendices contain other backup in-
formation including details of the availability of implementations, common Hufs
errors and a comparison between functional, imperative and 00 programming.
xvi Preface
Other support materials appear on the Web site for the book:
Outline
In writing larger programs, it is imperative that users can define types for themselves.
Haskell supports this in two ways. Algebraic types like trees are the subject of Chapter
14. which covers all aspects of algebraic types from design and proof to their interaction
with type classes, as well as introducing numerous examples of algebraic types in
practice. These examples are consolidated in Chapter 15, which contains thc casc stidy
of coding and decoding of information using a Huffman-style code. The foundations
of the approach are outlined before the implementation of the case study. Modules are
used to break the design into manageable parts, and the more advanced features of the
Haskell module system are introduced at this point.
An abstract data ty pe (ADT) providcs access to an implementation through a restricted
set o f functions. Chapter 16 explores the ADT mechanism of Haskell and gives
numerous examples of how it is used to implement queues, sets, relations and so forth.
as well as giving the basics of a simulation casc study.
Chapter 17 introduces lazy evaluation in Haskell which allows programmers n dis-
tinctive style incorporating backtracking and infinite data structures. As an example of
backtracking there is a parsing case study, and infinite lists are used to give 'process
style' programs as well as a random-number generator.
Haskell programs can perform input and output by means of the I0 types. Their
members - examined in Chapter 18 - represent action-based programs. The programs
are most readily written using the do notation, which is introduced at the start of the
chapter, and illustrated through a series of cxamplch, culminating in an interactivc li-ont-
end to the calculator. The foundations of the do notation lie in monads. which can also
be used to do action-based programming of a number of different flavours. some of
which are examined in the second half of the chapter.
The text continues with an examination in Chapter 19 of program behaviour, by which
we mean the time taken for a program to compute its result, and the space used in that
calculation. Chapter 20 concludes by surveying various applications and extensions oI'
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 frst examines links with
functional and 00 programing, and the second gives a glossary of commonly used
terms in functional programming. The others include a summary of Haskell operators
and Hugs errors, together with help on understanding programs and details of the v;I nous
'
implementations of Haskell.
The Haskell code for all the examples in the book, as well as other background
materials, can be downloaded from the Web site for the book.
The second edition of the book incorporates a number of substantial changes, for iI
variety of reasons.
xviii Preface
Introduction
Another consequence of the first-edition approach was that it took some hundred pages
before any substantial examples could be introduced; i n this edition there is an example
of pictures in Chapter I which both forms a more substantial case study and is used
to preview the ideas of' polymorphism, higher-order functions and type abstraction
introduced later in the text. The case study is revisited repeatedly as new material is
brought in, showing how the same problems can be solved more effectively with new
machinery, as well as illustrating the idea of program verification.
The introduction also sets out rnore clearly some of the basic concepts of functional
programming and Haskell, and a separate Chapter 2 is used to discuss the Hugs system,
Haskcll scripts and modules and so forth.
Haskell 98
The book now has an emphasis on using the full resources of Haskell 98. Hugs now
provides an almost complete implementation of Haskell, and so as far as systems are
concerned Hugs is the exclusive subject. In most situations Hugs will probably be the
implementation of choice for teaching purposes, and if it is not used, it is only the system
descriptions which need to be ignored, as none of the language features described are
Hugs-specific.
The treatment of abstract data types uses the Haskell mechanism exclusively, rather
than the restricted type synonym mechanism of Hugs which was emphasized in the
first edition. The material on 110 now starts with the do notation, treating it as a
mini language for describing programs with actions. This is followed by a general
introduction to monads, giving an example of monadic computation over trees which
again uses the do notation.
Finally, functions in the text are given the same names as they have in the prelude
or libraries. which was not always the case in the first edition. Type variables are the
Preface xix
To the reader
It is always an option to cover only a subset of the topics, and this can be achieved
by stopping before the end; the rest of this section discuswh i n more detail other ways
of trimming the material.
There is a thread on program verification which begins with Chapter 8 and continues
in Sections 10.9, 14.7, 16.10 and 17.9; this thread is optional. Similarly, Chaptcr 19
gives a self-contained treatment of program time and space behaviour which is also
optional.
Some material is more technical. and can be omitted on (at least the) first reading.
This is signalled explicitly in the text, and is contained in Sections 8.7 and part of
Section 13.2.
Finally, it is possible to omit some of the examples and case studies. For example,
Scctions 6.3 and 6.4 are extended sets of exercises which need not be covered; the text
processing (Section 7.6) and indexing (Section 10.8) can also be omitted - their roles
are to provide reinforcement and to show the system used on rather larger examples. In
the later chapters, the examples in Sections 14.6 and 16.7-16.9 and in Chapter 17 can
be skipped, but paring too many examples will run the risk of losing some motivating
material.
Chapter 15 introduces modules in the tirst two sections; the remainder is the Huffman
coding case study, which is optional. Finally, distributed through thc final chapters are
the calculator and simulation case studies. Thcsc are again optional, hut omission of
the calculator case study will remove an important illustration of parsing and algebraic
and abstract data types.
Acknowledgements
For feedback on the first edition, 1 am grateful particularly to Ham Richards, Bjorn von
Sydow and Kevin Hammond and indeed to all those who have pointed out errata in that
text. In reading drafts of the second edition, thanks to Tim Hopkins and Peter Kcnny
as well as the anonymous referees.
Emma Mitchell and Michael Stsang of Addison-Wesley have supported this second
edition from its inception; thanks very much to them.
Particular thanks to Jane for devotion beyond the call of duty in reading and com-
menting very helpfully on the first two chapters, as well as for her support over the last
year while 1 have been wr~tingthis edition. Finally, thanks to Alice and Rory who have
more than readily shared our home PC with Haskell.
\
Sinwn Tl~ompson
Cmzterbury, January I999
( Chapter 1 )
Introducing functional
programming
Computers and modelling
What is a function?
Pictures and functions
TYpes
The Haskell programming language
Expressions and evaluation
Definitions
Function definitions
Looking forward: a model of pictures
Proof
Types and functional programming
Calculation and evaluation
This chapter sets the scene for our exposition of functional programming in Haskell. The
chapter has three aims.
We want to introduce the main ideas underpinning functional programming. We
explain what it means to be a function and a type. We examine what it means to find
the value of an expression, and how to write an evaluation line-by-line. We look at
how to define a function, and also what it means to prove that a function behaves in
a particular way.
We want to illustrate these ideas by means of a realistic example; we use the example
of pictures to do this.
2 Introducing functional programming
Finally, we want to give a preview of some of the more powerful and distinctive
ideas in functional programming. This allows us to illustrate how it differs from other
approaches like object-oriented programming, and also to show why we consider
functional programming to be of central importance to anyone learning computing
science. As we proceed with this informal overview we will give pointers to later
chapters of the book where we explain these ideas more rigorously and in more detail.
programs
4 9
is
implementation
1 computer
What i s a function? 3
Our task in this text is programming, so we shall be occupied with the upper half of the
diagram above, and not the details of implementation (which are discussed in Peyton
Jones 1987; Peyton Jones and Lester 1992).
Our subject here is functional programming, which is one of a number of different
programming styles or paradigms; others include object-oriented (OO), structured and
logic programming. How can there be different paradigms, and how do they differ'? One
very fruitful way of looking at programming is that it is the task of modelling situations
-either real-world or imaginary -within a computer. Each programming paradigm will
provide us with different tools for building these models; these different tools allow us
- or force us - to think about situations in different ways. A functional programmer
(What is a function?
A function is something which we can picture as a box with some inputs and an output,
thus:
inputs
output
The function gives an output value which depends upon the input value(s). We will
often use the term result for the output, and the terms arguments or parameters for
the inputs.
A simple example of a function is addition, +, over numbers. Given input values 1 2
and 34 the corresponding output will be 46.
-1-
l2
34
inputs
output
a function giving the distance by road (output) between two cities (inputs);
a supermarket check-out program, which calculates the bill (o~itput)from a list of
bar codes scanned in (input); and
a process controller, which controls valves in a chemical plant. Its inputs are the
information from sensors, and its output the signals sent to the valve actuators.
4 Introducing functional programming
We mentioned earlier that different paradigms are characterized by the different tools
which they provide for modelling: in a functional programming language functions
will be the central component of our models. We shall see this in our running example
of pictures, which we look at now.
where we have illustrated the effect of this reflection on the 'horse' image
Some filnctions will take two arguments, among them a function to scale images,
- -
a above
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; f lipV will transform a picture into a picture; s c a l e 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 apply the same functions to them. It is reasonable to find the
larger of two numbers, but not to compare a number and a picture, for instance.
If we look at the addition function, +. it only makes sense to add two numbers 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, thus:
6 Introducing functional programming
Int
J +
Int
*
Int
The diagram indicates that + takes two whole numbers (or Integers) as arguments and
gives a whole number as a result. In a similar way, we can label the s c a l e function
Plcl-ure
9- Picture
Int scale D
?r
to indicate that its first argument is a P i c t u r e and its second is an I n t , with its result
being a P i c t u r e .
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 operation
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 P i c t u r e and the other of type I n t .
In modelling a problem situation, a type can represent a concept like 'picture', while
a function will represent one of the ways that such objects can be manipulated, such as
placing one picture above another. We shall return to the discussion of types in Section
1.1 1.
Haskell (Peyton Jones and Hughes 1998) is the functional programming language which
we use in this text. However, many of the topics we cover are of more general interest
and apply to other functional languages (as discussed in Chapter 20), and indeed are
lessons for programming in general. Nevertheless, [he book is of most value as a text
on fi1nctic)nal programming in the Haskell language.
Haskell is named after Haskell B. Curry who was one of the pioneers of the A
calculus (lambda calculus), which is a mathematical theory of functions and has been
an inspiration to designers of a number of functional languages. Haskell was first
specified in the late 1980s. and has since gone through a number of revisions before
reaching its current 'standard' state.
There are a variety of implementations of Haskell available; in this text we shall
use the Hugs (1998) system. We feel that Hugs provides the best environment for thc
learner, since it is freely available for PC, Unix and Macintosh systems, it is efficient
and compact and has a flexible user interface.
Hugs is an interpreter - which means loosely that it evaluates expressions step-by-
step as we might on a piece of paper - and so it will be less efficient than a compiler
which translates Haskell programs directly into the machine language of a computer.
Compiling a language like Haskell nllows its programs to run with a speed similar to
those written in more conventional languages like C and C++. Details of all the different
Expressions and evaluation 7
implementations of Haskell can be found in Appendix E and at the Haskell home page,
http://www.haskell.org/.
From now on we shall be using the Haskell programming language and the Hugs
system as the basis of our exposition of the ideas of functional programming. Details
of how to obtain Hugs are in Appendix E. All the programs and examples used in the
text can be downloaded from the Web page for this book,
expression value
( 7 - 3 ) *2 8
evaluation
to give the value 8. This expression is built up from symbols for numbers and for
functions over those numbers: subtraction - and multiplication *; the value of the
expression is a number. This process of evaluation is automated in an electronic
calculator.
In functional programming we d o exactly the same: we evaluate expressions to givc
values, hut 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 f lipV to
the horse. This function application is written by putting the function followed by its
argument(s), thus: f lipV horse and then evaluation will give
expression value
flipV h o r s e *-D ; ,,
evaluation
1/ I
::-------
Definitions
name : : type
name = expression
as in the example
size : : Int
size = 12+13
which associates the name on the left-hand side, size,with the value of the expression
on the right-hand side, 25, a value whose type is Int, the type of whole numbers or
integers. The symbol ': :' should be read as 'is of type', so the first line of the last
definition reads 'size is of type Int'. Note aIso 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 functions
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 detinitions 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 function
invertcolour to the horse, thus giving
r o t a t e H o r s e :: P i c t u r e
r o t a t e H o r s e = flipH (flipV h o r s e )
and we can picture the evaluation of the right-hand side like this
and so by doing some arithmetic we can conclude that the value of the expression is 8.
The definitions we have seen so far are simply of constant values; we now turn our
attention to how functions are defined.
Function definitions
We can also define functions, and we consider some simple examples now. To square
an integer we can say
s q u a r e : : I n t -> I n t
s q u a r e n = n*n
where diagran~n~atically
the definition is represented by
Int Int
n square n*n
A
The first line of the Haskell definition of s q u a r e declares the type of the thing being
defined: this states that s q u a r e is a function - signified by the arrow -> -which has a
10 Introducing functional programming
single argument of type I n t (appearing before the arrow) and which returns a result of
type I n t (corning after the arrow).
The second line gives the definition of the function: the equation states 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 rwlue of'n, s o that it will hold whatever integer
expression we put in the place of n, having the consequence that, for instance
square 5 = 5*5
and
This is the way that the equation is used in evaluating an exprewion which uses square.
If wc are required 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 xl x2 ... xk = e
The variables used on the left-hand side of an equation defining a function are called the
formal parameters because they stand for arbitrary values of the parameters (or actual
parameters. as they are sometimes known). We will 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 which is meant when 'parameter' is used.
Accompanying the definition of the function is a declaration of its type. This will
take the following form, where we use the function s c a l e over pictures for illustration:
r o t a t e : : P i c t u r e -> P i c t u r e
r o t a t e p i c = f l i p H (f lipV p i c )
rotateHorse :: P i c t u r e
rotateHorse = r o t a t e horse
r o t a t e : : P i c t u r e -> P i c t u r e
rotate = flipH . flipV
The ' . ' in the definition signifies function composition. in which the output of onc
function becomes the input of another. I n pictures,
we see the creation of a new function by connecting together the input and output of
two given functions: obviously this suggests inany other ways of connecting together
functions, many of which we will look at in the chapters to come.
The direct combination of functions gives us the first example of the power of
functional programming: we are able to combine functions using an operator like
' . ' just as we can combine numbers using '+'. We use the tern1 'operator' here rather
12 Introducing functional programming
than 'function' since ' . ' is written between its arguments rather than before them: we
discuss operators i n more detail in Section 3.7.
The direct combination of functions by means of the operator ' . ' which we have
seen here is not possible i n orher prograniming paradigms, or at least it would be a11
'advanced' aspect of the language, rather than appearing on page 1 I of an introducrory
text.
Type abstraction
Before moving on. we point out another crucial issue which we will explore later in thc
book. We have just given definitions of
blackHorse : : Picture
rotate : : Picture -> Picture
which use the type Picture and some functions already def ned over it. namely f lipH
and f lipV. We were able to write the definitions of blackHorse and rotate ~ * i / / ~ o l r /
k~lo\vitlgm y t h i l l g ( ~ h o ~the
l t details of the type Picture oraboul how the 'flip' functions
work over pictures. save for the fact that they behavc as we have described.
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 detined. The advantngc ol'this
is that the definitions we give apply horwver pictures are modelled. We might choose
to model them in different ways in different situations; whatever the case. the I'unction
composition f lipH . f lipV will rotate a picture through 180". Chapter 16 discusses
this in more detail, and explains the Haskell mechanism to si~pporttype abstraction. I n
the next section we preview other important features of Haskell.
We include this section in the first chapter of the book for two reasons. To start with.
we want to describe one straightforward way in which Pictures can be modelled in
Haskell. Secondly, we want to provide an informal preview of a number of aspects of
Haskell which make i t n powerful and distinctive programming tool. As we go alon:
we will indicate the parts of the book where we expand on the topics first introduced
here.
Our model consists of two-dimension& monochrome pictures built from characters.
Characters are the individual letters, digits, spaces and so forth which can be typed at
the computer keyboard and which can also be shown on a computer screcn. In Haskell
the characters are given by the built-in type Char.
This model has the advantage that it is straightforward to view these pictures on a
computer terminal window (or if we are using Windows, in the Hugs window). On the
other hand, there are other more sophisticated models; details of these can be fi)und at
the Web site for the book, mentioned on page 7.
Our version of the horse picture, and the same picture flipped in horizontal and
vertical mirrors will be
Looking forward: a model of pictures 13
Finally, the rrs~rltof applying map to reverse is itself a function. This covered in
Chapter 10.
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 multitude
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
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:'
above = (++)
To place two pictures sideBySide we have to join corresponding lines together, thus
and this is defined using the function zipwith. This function is defined to 'zip together'
corresponding elements of two lists using - in this case - the operator ++.
' The operator ++ is surrounded by parenthcws ( ... in this definition s o that i t i.; interprctcd a s a function:
w e say more about this in Section 3.7.
Proof 15
we can check whether or not a2=b2+c2holds. 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, 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 will take an
example over the Picture type to illustrate what can be done. We saw in Section 1.8
that we can define
but it is interesting to observe that if we reverse the order in which the flip functions
are applied then the composition has the same effect, as illustrated here:
Moreover, we can look at our implementations o f f lipV and f lipH and give a logical
proof that these functions have the property labelled (f lipprop) above. The crux of
the argument is that the two functions operate independently:
the function f lipV affects each line but leaves the lines in the same order while
the function f lipH 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.
16 Introducing functional programming
Proof is possible for most programming languages, but it is substantially easier for
functional languages than for any other paradigm. Proof of program propertics will
be a t h e m in this text, and we start by exploring proof for list-processing functions in
Chapter 8.
What benefit is there i n having a proof of a property like (flipProp)? I t b' rive us
~ ~ r t c r i n tthat
y our functions have a particular property. Contrast this with the usual
approach in computing where we test the value of a function at a selection of places;
this only gives us the assurance that the function has the property we seek at the test
points, and in principle tells us nothing about the function in other circumstances. There
are safety-critical situations in which it is highly desirable to be sure that a program
behaves properly, and proof has a role here. We are not. however, advocating that
testing is unimportant - merely that testing and proof have complementary roles to play
in software development.
More specifically. (f lipprop) means that we can be sure that however we apply
the functions f lipH . f lipV and f lipV . f lipH they will have the same effect.
We could therefore transform a program using f lipH . f lipV into one using the
functions composed in the reverse order, f lipV . f lipH,and bc certain that the new
pmgram will have exactly the same effect as the old, Ideas like this can be used to
good effect within implementations of languages, and also i n developing programs
theniselves, as we shall see in Section 10.9.
What is the role of types in functional programming'? Giving a type to a function first
of all gives us crucial inti)rmation about how it is to be used. If we know that
scale : : Picture -> Int -> Picture
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 w1ue.s of scale or horse - all that we need to know to perform the check
Calculation and evaluation 17
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 misunderstanding
the prohlem, are made by both novice and experienced programmers. A type system
therefore helps the user to write correct programs, and to avoid a large proportion of
programming pitfalls, both obvious and subtle.
We have explained that Hugs can be seen as a general calculator, using the functions
and other things defined in a functional program. When wc evaluate an expression like
This we do by replacing the unknown n in the definition (dbl) by the expression (3+1),
giving
Now we can replace double (3+1) by 2* (3+1) in ($) , and evaluation can continue.
One of the distinctive aspects of functional programming is that such a simple 'cal-
culator' model effectively describes computation with a functional program. 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.
-
23
r i . ~
- (double (3+1))
23 - (2*(3+1))
23 - (2*4)
using (dbl)
arithmetic
- 23-8
15
arithmetic
arithmetic
where we have used '-4 ' 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 expression
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 thus:
18 Introducing functional programming
In what is to come, when we introduce a ncw feat~ireof Haskell we shall show how i t
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 new concepts.
Summary
As we said at the ctart, this chapter has three aims. We wanted to introduce some of
the fundamental ideas of functional programming; to illu\trate them with the example
of pictures, and also to give a flavour of what it is that is distinctive about functional
programming. To cum up the definition\ we have seen,
In the remainder of the book we will cxplore different ways of defining new types and
functions, as well as following up the topics of polymorphism, functions as arguments
and results, data abstraction and proof which we have touched upon in an informal way
here.
Getting started with
Haskell and Hugs
2.1 A first Haskell program
2.2 Using Hugs
2.3 The standard prelude and the Haskell libraries
2.4 Modules
2.5 A second example: Pictures
2.6 Errors and error messages
We begin the chapter by giving a first Haskell program or script, which consists of the
numerical examples of Chapter 1. As well as definitions, ascript will contain comments.
20 Getting started with Haskell and Hugs
The p u r p o s e of t h i s s c r i p t i s
- t o i l l u s t r a t e some s i m p l e d e f i n i t i o n s
over i n t e g e r s ( I n t ) ;
- t o g i v e a f i r s t example of a s c r i p t .
-- The v a l u e s i z e i s a n i n t e g e r ( I n t ) , d e f i n e d t o be
-- t h e sum of t w e l v e and t h i r t e e n .
size :: Int
s i z e = 12+13
-- The f u n c t i o n t o s q u a r e a n i n t e g e r .
s q u a r e : : I n t -> I n t
Square n = n*n
-- The f u n c t i o n t o d o u b l e a n i n t e g e r .
d o u b l e : : I n t -> I n t
d o u b l e n = 2*n
-- An example u s i n g d o u b l e , s q u a r e and s i z e .
example : : I n t
example = d o u b l e ( s i z e - s q u a r e ( 2 + 2 ) )
The purpose of t h i s s c r i p t i s
- t o i l l u s t r a t e some simple d e f i n i t i o n s
over i n t e g e r s ( I n t ) ;
- t o g i v e a f i r s t example of a l i t e r a t e s c r i p t .
The f u n c t i o n t o square an i n t e g e r .
The f u n c t i o n t o double an i n t e g e r .
> example : : I n t
> example = double ( s i z e - square (2+2))
F i r s t s c r i p t . hs, in Figure 2.1. Scripts of this style are stored in files with an extension
' .hs'.
C o n ~ n ~ e nare t s indicated in two ways. The symbol '--' begins a comment which
occupies the part of the line to the right of the symbol. Comments can also be enclosed
by the symbols '(-' and '-1'. These comments can be of arbitrary length, spanning
more than one line, as well as enclosing other comments; they are therefore called
nested comments.
22 Getting started with Haskell and Hugs
a Using ~ u g s
Hugs is a Haskell implementation which runs on both PCs (under Windows 95 and NT)
and Unix systems, including Linux. It is freely available via the Hashell home page,
Starting Hugs
To start Hugs on Unix, type hugs to the prompt; to launch Hugs using a particular file,
type hugs followed by the name of the file in question, as in
hugs F i r s t L i t e r a t e
Using Hugs 23
where we have indicated the machine output by using a slanted font; user input appears
in unslanted form. The prompt here, Main,, will be explained in Section 2.4 below.
As can be seen from the examples, we can evaluate expressions which use the defini-
tions in the current script. In this case it is F i r s t L i t e r a t e . l h s (or F i r s t s c r i p t . h s ) .
' This assumes that the appropriate registry entries have been matlc; wc work here with the stndard
in.;tallation of Hugs as discussed in Appendix E.
24 Getting started with Haskell and Hugs
One of the advantages of the Hugs interface is that it is easy to experiment with
functions, trying different evaluations simply by typing the expressions at the keyboard.
If we want to evaluate a complex expression, it might be sensible to add it to the program,
as in the definition
t e s t :: Int
t e s t = double 320 - square ( s i z e - double 6)
All that we then need to do is to type t e s t to the Main> prompt.
Hugs commands
Hugs commands begin with a colon, ':'. A summary of the main commands follows.
All the ':' commands can be shortened to their initial letter, giving : 1 p a r r o t and
so forth. Details of other commands can be found in the comprehensive on-line Hugs
documentation which can be read using a Web browser. On a standard Windows
installation it is to be found at
but in general you will need to consult locally to find its location on the system which
you are using.
Editing scripts
Hugs can be connected to a 'default' text editor, so that Hugs commands such as : e d i t
and : f i n d use this editor. This may well be determined by your local set-up. The
'default' default editor on Unix is v i ; on Windows systems e d i t or notepad might
be used. Details of how to : s e t values such as the default editor are discussed in
Appendix E.
Using the Hugs : e d i t command causes the editor to be invoked on the appropriate
file. When the editor is quit, the updated file is loaded automatically. However, it is
Using Hugs 25
more convenient to keep the editor running in a separate window and to reload the tile
by:
writing the updated file from the editor (without quitting it), and then
reloading the file in Hugs using : r e l o a d or : r e l o a d filename.
In this way the editor is still open on the tile should it need further moditication.
We now give some introductory exercises for using Hugs on the first example
programs.
square s i z e
square
double (square 2)
$$
square (double 2)
23 - double (3+1)
23 - double 3+1
$$ + 34
13 ' d i v ' 5
13 'mod' 5
On the basis of this can you work out the purpose of $$?
Task 2
Use the Hugs command : t y p e to tell you the type of each of these, apart from $$.
Task 3
What is the effect of typing each of the following?
double square
2 double
Task 4
Edit the file F i r s t L i t e r a t e . l h s to include definitions of functions from integers to
integers which behave as follows.
26 Getting started with Haskell and Hugs
The function should double its input and square the result of that.
The function should square its input and double the result of that.
Your solution should include declarations of the types of the functions.
(2.4)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
n~odules.
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:
module Ant where
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.3, 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 modules are best
used to support the design of software systems.
We are now in a position to explain why the Hugs prompt appears as Main,. The
prompt shows the name of the top-level module currently loaded in Hugs, and in the
absence of a name for the module it is called the 'Main' module, discussed in Chapter
15.
In the light of what we have seen so far, we can picture a Hugs session thus:
input of
and output
of results
The current script will have access to the standard prelude, and to those modules 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 Hugs, providing
expressions to evaluate and other commands and receiving the results of the evaluations.
The next section revisits the picture es;i~nplcol' Chapter I, which is used to give a
practical illustration of modules.
The running example in Chapler 1 was of pictures, and in Figure 2.4 we show parts
of a script implementing pictures. We have omitted some of the detinitions, replacing
them with ellipses ' . . . '. The module here is called Pictures, and can be downloaded
from the Web page for this text, mentioned on page 22. This module is imported into
another module by the statement
import Pictures
which is used to display a Picture on the screen. The type I 0 is a part of the Hashell
mechanism for inputloutput (110). We examine this mechanic~nin detail in Chapter I 8:
for the present it is enough to know that if horse is the name of the picture used in the
earlier examples, then the effect of the function application
printpicture horse
is the display
. .##. ..
. . . . .# # . . # . .
. . .# # . . . . . # .
. .# . . . . . . . # .
. #...#...#.
. .# . . . # # # . # .
.#....#..##.
. .# . . . # . . . . .
. . .# . . . # . . . .
. . . .# . . # . . . .
. . . . .# . # . . . .
. . . . . . ## . . . .
first seen in Chapter 1. Any Picture can be printed in a similar way.
In the remainder o f this section we present a series of practical exercise\ designed to
use the module Pictures . lhs.
(Exercises)
2.1 Define a module UsePictures which imports Pictures and contain? defini-
tions of blackHorse, rotate and rotateHorse which can use the detinitions
imported from Pictures.
In the remaining questions you are expected to add other definitions to your
module UsePictures.
2.2 How would you make a definition of a black rectangle? How could yo11 d o
this without using white, but assuming that you have a function superimpose
defined as discussed on page S ?
2.3 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 x 8 board of alternating square\'?
30 Getting started with Haskell and Hugs
2.4 Three variants of the last picture which involve the 'horse' pictures are
2.5 Give another variant of the 'horse' pictures in the previous question, and show
how it could be created. Note: a nice variant is
No system can guarantee that what you type is sensible, and Hugs is no cxccption.
If something is wrong, either in an expression to be evaluated or in a script, you will
receive an error message. Try typing
to the Hugs prompt. The error here is in the syntax, and is like a sentence in English
which does not have the correct grammatical structure, such as 'Fishcake our camel'.
The expression has too few parentheses: after the '4', a closing parenthesis is
expcctcd, to niatch with the opening parenthesis before '3'. The error message reflects
this by saying that what follows '4' is unexpected:
double square
This gives a type error. since double is applied to the function square, rather than an
integer:
Errors and error messages 31
ERROR: Type e r r o r i n a p p l i c a t i o n
*** expression double s q u a r e
*** term square
*** type I n t -> I n t
*** does n o t match : Int
The message indicates that something of type I n t was expected, but something of type
I n t -> I n t was present instead. Here double expects something of type I n t as its
argument, but s q u a r e of type I n t -> I n t is found in the place of an integer.
When you gel an error message likc the one above you need to look at how the term,
in this case s q u a r e of type I n t -> I n t , does not match the context in which it is
used: the context is given in the second line (double s q u a r e ) and the type required
by the context, I n t , is given in the last line.
Type errors do not always give rise to such well-structured error messages. Typing
either 4 double or 4 5 will give rise to a message like
ERROR: . . . i s n o t a n i n s t a n c e of c l a s s . . .
We will explore the technical details behind these messages in a later chapter; for now
it is sufficient to read these as 'Type Error!'.
The last kind of error we will sec are program errors. Try the expression
We cannot divide by zero (what would the result be'!) and so we get the messagc
Program e r r o r : {primDivInt 4 0)
indicating that a division of 4 by 0 has occurred. More details about the error mcssagcs
produced by Hugs can be found in Appendix F.
Summary
The main aim of this chapter is practical, to acquaint the reader with the Hugs imple-
mentation of Haskell. We have seen how to write simple Hugs programs; to load them
into Hugs 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 illustrate
modules in the case study of P i c t u r e s introduced in Chapter 1.
We concluded the chapter with an overview of the possible syntax, type and program
errors in expressions or scripts submitted to Hugs.
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 the Hugs interpreter.
( Chapter 3 ]
Basic types and
definitions
3.1 The Booleans: Bool
3.2 The integers: I n t
3.3 Overloading
3.4 Guards
3.5 The characters: Char
3.6 Floating-point numbers: F l o a t
3.7 Syntax
We have now covered the basics of functional programming and have shown how simple
programs are written, modified and run in Haskell. 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 I n t type in
use; we shall cover this and also the type F l o a t of floating-point fractional numbers.
Often in programming we want to make a choice of values, according to whether or
not a particular condition holds. Such conditions include tests of whether one number
is greater than another; whether two values are equal, and so on. The results of these
tests - True if the condition holds and F a l s e if it fails - are called the Boolean values,
after the nineteeth-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.
Finally, we look at the type of characters - individual letters, digits, spaces and so forth
- which are given by the Haskell type Char.
The Booleans: BOO^ 33
The chapter provides reference material for the basic types; a reader may skip the
treatment of Float and much of the detail about Char, 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.
The Boolean values True and F a l s e represent the results of tests, which might, for
instance, compare two numbers for equality, or check whether the first is smaller than the
second. The Boolean type in Haskell is called Bool. The Boolean operators provided
in the language are:
&& and
II or
not not
Because Bool contains only two values, we can detine the meaning of Roolean operators
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 F a l s e && True is F a l s e and that the value of F a l s e I I True is True.
Booleans can be the arguments to or the results of functions. We now look at some
examples. 'Exclusive or' is the function which returns True exactly when one but not
both of its arguments have the value True; it is like the 'or' of a restaurant menu: you
may have vegetarian moussaka or fish as your main course, but not both! The 'built-in
or', I 1 , is 'inclusive' because it returns True if either one or both of its arguments arc
True.
We can picture the function definition using boxes for functions, and lines for values.
as we saw i n Chapter 1. Lines coming into a function box represent the arguments, and
the line going out the result.
34 Basic types and definitions
Boolean values can also bc comparcd for equality and inequality using the operators
==and /=, which both havc the type
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 equations
defining e x 0 r :
exOr True x = n o t x
exOr F a l s e x = x
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 F a l s e on the left-hand side of equations are often
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
examine in detail in Section 7.1.
The integers: I n t 35
( Exercises 7
3.1 Give another version of the definition of 'exclusive or' which works informally
thus: 'exclusive or of x and y will be True if either x is True and y is F a l s e . or
vice versa'.
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.
which returns the result True except when both its arguments are True. Give a
diagram illustrating one of your definitions.
3.5 Give line-by-line calculations of
The Haskell type I n t contains the integers. The integers are the whole numbers, used
for counting; they are written thus:
The I n t type represents integers in a fixed amount of space, and so can only represent
a tinite range of integers. The value maxBound gives the greatest value in the type,
which happens to be 2147483647. For the majority of integer calculations these fixed
si7e numbers are suitable, but if larger numbers are required we may use the I n t e g e r
type, which can accurately represent whole numbers of any size.'
' We choose to work with I n t here because various standard Haskell functions which w e introduce later in
the chapter use the I n t type.
36 Basic types and definitions
We do arithmetic on integers using the following operators and functions; the oper-
ations we discuss here also apply to the I n t e g e r type.
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 nuinber division; for example d i v 14 3 is 4.
This can also be written 14 ' d i v ' 3.
mod The remainder from whole nuinber division; for
example mod 14 3 (or 14 'mod' 3) is 2.
a bs 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.
n e g a t e -34
is interpreted as 'negate minus 34' and thus leads to the Hugs error message
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: n e g a t e (-34).
Sec Section 3.7 for more details.
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 F a l s e . 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)
Overloading 37
A simple example using these definitions is a function to test whether three Ints are
equal.
threeEqual : : Int -> Int -> Int -> Bool
threeEqua1 m n p = (m==n) && (n==p)
( Exercises )
3.6 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 doe5 on some example inputs.
3.7 Define a function
fourEqual :: Int -> Int -> Int -> Int -> Bool
which returns the value True only if all four of its arguments are equal.
Give a definition of fourEqua1 modelled on the definition of threeEqua1
above. Now give a definition o f f ourEqualwhich u . w v the function threeEqual
in its detinition. Compare your two answers.
3.9 Give line-by-line calculations of
Overloading
Both integers and Booleans can be cornpared for equality, and the same symbol == is
used for both 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
38 Basic types and definitions
('") Guards
Here we explore how condition\ or guards are used to give alternatives i n the definition\
of functions. A guard is a Boolean expression, and these expressions are used to expre\\
variou\ case\ in the definition of a function.
We take a\ a running example in thic \ection functions which compare integer\ for
\ire, and \tart 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 common value
the maximum.
How do we read a definition like this, which appears in the Haskell prelude'?
rnax x y equals x
if the guard is True
rnax x y
a'
I x > = y = X
I otherwise = Y
I n 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 F a l s e , then we look at
the second, and so on. An o t h e r w i s e guard will hold whatever the arguments, so that
in the case of rnax 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.
Guards 39
tests whether x is the rnaxi~ni~ni of the three inputs: if it is True the corresponding result
is x. If the guard fails, then x is not the maximum, so there has to be a choice between
y and z. The second guard is therefore
If this holds, the result is y; otherwise the result is z . We will go back to thc cxample
ofmaxThree in Section 4.1.
We first gave a general form for simple function definitions i n Chapter I ; we can now
strengthen this to give a general form for function definitions with guards in Figure 3.1.
Note that the o t h e r w i s e is not compulsory.
f f theformal
being defined parameters
, . I gl = el):
;;
,,I>@
....
= e2fi:.,
......
.;.,',.' 1 otherwise = e '....*
'
..
.
,;a
a,
A.
""
*/' \'a
Figure 3.1 The general form for function definitions with guards.
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
to its arguments we need to know which of the cases applies, and to do this we need to
40 Basic types and definitions
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
- ??
4
True
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
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 Hugs
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.
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
i s m if the condition is True and is n if the condition is False, so that the expression
if False then 3 else 4 has the value 4, and in general
if x >= y then x else y
will be the maximum of x and y. This shows that we can write max in a different way
thus:
max : : Int -> Int -> Int
max x y
= if x >= y then x else y
The characters: Char 41
We tend to u\e thc guard for111rilther than this, but we will see examples below where
the use of if . . . then . . . else . . . is more natural.
/ Note: Redefining prelude functions
The max function is detined in the prelude, Prelude.hs, and if a definition
max : : Int -> Int -> Int
appears in a script maxDef .hs then this dcfi nition will conflict with thc prelude
definition, leading lo the Hugs error message
To redefine the prelude functions max and min, say, the line
which overrides the usual import of the prelude should be included a1 the top of
the file maxDef . hs, after its module statement (if any).
Many of the functions detined in this text are in fact included in the prelude. and
so this technique needs to be used whenever you want to redetine one of these.
max ( 3 - 2 ) (3*8)
maxThree (4+5) (2*6) (100 'div' 7 )
There is a standard coding for characters as integers, called the ASCll 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 ' \97 ' have the same meaning. ASCll has recently been extended
to the Unicode standard, which contains characters from fonts other 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 small
lettcr to a capital an offset needs to be added to its code:
offset : : Int
offset = ord 'A' - ord 'a'
Note that the off set 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 changc the definition of off set, 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 acharacter
is a digit thus:
The standard prelude contains a number of conversion functions like toupper, and
discrimination functions like isDigit; details can be found in the file Prelude.hs.
Other useful functions over Char are to be found in the library Char.hs.
Floating-point numbers: F l o a t 43
( Exercises 1
3.1 2 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.
The numbers are called floating point because the position of the decimal point is not
the same for all F l o a t s ; 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
44 Basic types and definitions
Thi\ representation i \ more convenient than the decimal numerals above for very large
and m a l l numbers. Concider the number 2 . 1444. This will need well over a hundred
digits before the decimal point, and this would not be pos4ble i n decimal notation
of limited \ize (uwally 20 digits at most). In scientific notation, it will be written a\
l.l62433e+l43.
Haskell provides a range of operators and functions over F l o a t in the \tandad
prelude. The table in Figure 3.2 gives their name, type and a brief description of their
behaviour. Included are the
Haskell can be used as a numeric calculator. Try typing the expression which follows
to the Hugs prompt:
s i n (pi/4) * sqrt 2
I n t -> I n t -> I n t
F l o a t -> F l o a t -> F l o a t
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 12.
since we are trying to add quantities of two different types. We have to converl
the I n t to a F l o a t to perform the addition. thus:
fromInt ( f l o o r 5.6) + 6 . 7
( Exercises 1
3.14 Give a function to return the average of three integers
which returns how many of its inputs are larger than their average value.
The remainder of the questions look at solutions to a quadratic equation
46 Basic types and definitions
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.
3.16 Using your answer to the last question, write a function
that given the coefficients of the quadratic, a, b and c, will return how inany
roots the equation has. In the case that the equation has every number a root you
should return the result 3.
3.1 7 The formula for the roots of a quadratic is
2a
Write definitions of the functions
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.
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.
Syntax 47
A script contains a series of definitions, one after another. How is it clear where
one definition ends and another begins? In writing English, the end of a sentence is
signalled by a fir11 stop, '.'. In Haskell the layout of the program is used to state where
one definition ends and the next begins.
Formally, a definition is ended by the first piece of text which lies at the same
indentation or to the left of the start of the definition.
When we write a definition, its first character opens up a box which will hold the
definition, thus
f ystery x = x*x
t ystery x = x*x
. . . until something is found which is on the line or to the left of the line. This closes
the box, thus
ystery x = x*x
next x = ...
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:
We see error mesages involving ' ; ' wen if we have not used it ourselves. If we
break the offside rule thus:
funny x = x+
1
since internally to the system a '; ' is placed before the 1 to mark the end of the
definition, which does indeed come at an unexpected point.
Recommended layout
The offside rulc permits various different styles of layout. In this book for definitions
of any s i x we use the form
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 al\o that by
convention in this text we always specify the type of the function being detined.
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 vl v2 . . . Vn
( a long guard which may
go over a number of lines
= very long expression which goes
over a number of lines
I g2 = e2
...
Names in Haskell
Thus far in the book we have seen a variety of uses of names in definitions and
expressions. 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 uscd to begin type names, such as Int; constructors, such as True and
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
Fun x = x+l
gives the error message 'Undefined constructor function "Fun1".
There are some restrictions on how identifiers can be chosen. There is a small
collection of reserved words which cannot be used; these are
case class data default deriving do else if import in infix
infix1 infixr instance let module newtype of then type where
The special identifiers as, qualified, and hiding have special meanings i n certain
contexts but can be used as ordinary identifiers.
By convention, when we give names built up from more than one word, we capitalize
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 4 1.
Haskell is built on top of the Unicode character description standard, which allows
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
but expressions rapidly become difficult to read. Instead two extra properties of
operators allow us to write expressions uncluttered by parentheses.
Note that at the time of writing, the Hugs syqtern does not support Unicode characters.
50 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
associativity of addition. Because of this, we can write
for the sum, unambiguously. Not every operator is associative, however; what happens
when we write
left associative
right associative
Binding powers
The way in which an operator associates allows us to resolve expressions like
where the same operator occurs twice, but what is done when two different operators
occur, as in the following expressions?
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
A full table of the associativities and binding powers of the predefined Haskell operators
is given in Appendix C. In the section 'Do-it-yourself operators' below we discuss how
operators arc defined in scripts and also how their associativity and binding power can
be set or changed by declarations.
Syntax 51
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 include
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
(&&&I : : Int -> Int -> Int
x &&& y
I x > y = Y
I otherwise = x
The associativity and binding power of the operator can be specified; for det a1'1 s see
Appendix C.
52 Basic types and definitions
3.18 Rewrite your solutions to the earlier exercises to use the recommended layout.
3.1 9 Given the definitions
funny x = x+x
peculiar y = y
explain what happens when you remove the space in front of the p e c u l i a r
Summary
This chapter has introduced the base types I n t , F l o a t , Char and Boo1 together with
various built-in functions over them. We have seen how Boolean expressions - 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 tirst 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 )
Designing and writing
programs
4.1 Where do Istort? Designing a program in Haskell
4.2 Recursion
4.3 Primitive recursion in practice
4.4 General forms of recursion
4.5 Program testing
In this chapter we step back from discussing the details of Haskell and instead look a t 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.
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 optional examination
of more general forms of recursion.
Once we have written a definition we need to ask whether it does what it is intended
to do. We conclude the chapter by exploring the principles of program testing and
examining a number of examples.
( Definition 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.
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.
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.
as the name and type of the function returning the middle of three numbers without
having any idea about how we are going to define the function itself. Nevertheless, it is
progress, and also it gives us something to check our definition against when we havc
written it: if we manage to write a function middleNumber but it does not have the
type Int -> Int -> Int -> Int,then the function cannot be doing what it should.
Our definition of maxThree does a similar thing, replacing the condition for two values
with the condition for three, namely:
This way of using rnax is probably the first to spring to mind, but it is not the only way
that rnax can help us in defining maxThree.
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 rnax to give us the maximum of two. How could we use rnax 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,
56 Designing and writing programs
and in Haskell
What if I had any functions I wanted: which could I use in writing the solution?
This what l f . . . ? is a central question, because it breaks the problem into two parts.
First we have to give the solution c~ssumirzgwe 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.
Where do I start? Designing a program in Haskell 57
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
middleNumber : : Int -> Int -> Int -> Int
according to the first of the alternatives dchcribed on page 54. A model is given by the
delinition 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
I condition for x to be solution = x
1 condition for y to be solution = Y
Now, the problem comes in writing down the conditions, but here we say what jj' we
had a function to do this. Let us call it between. It has three numbers as arguments,
and a Boolean result,
between : : Int -> Int -> Int -> Boo1
and is clef ned so that between m n p is True if n is between m and p. We can complete
the definition of middleNumber now:
middleNumber x y z
1 between y x z = x
I between x y z = Y
I otherwise = z
The definition of the function between is lcft 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 solution 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 follow up this discussion in Chapter I 1 .
Exercises )
4.1 This question is about the function
maxFour : : Int -> Int -> Int -> Int -> Int
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.
58 Designing and writing programs
discussed in this section. The definition should be consistent with what we said
in explaining how middleNumber works. You also need to think carefully about
the different ways that one number can lie between two others. You might find
it useful to define a function
which returns how many of its three arguments are equal, so that
Think about what functions you have already seen - perhaps in the exercises -
howManyOfFourEqua1 : : Int -> Int -> Int -> Int -> Int
which is the analogue of howManyEqua1 for four numbers. You may need to
think uvlzut i f . . . ?.
(4.2 ) Recursion
Recursion is an important programming mechanism, in which a definition of a function
or other object refers to the object itself. This section concentrates on explaining the
idea o l recursion, and why it makes sense. In particular we give two complementary
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 u w d in practice.
Recursion 59
fac 6 = 1*2*3*4*5*6
Suppose we are also asked to write down a table of factorials, where we take the factorial
of zero to be one. We begin thus
n fac n
0 1
1 1
2 1*2 = 2
3 1*2*3 = 6
4 1*2*3*4 = 24
but we notice that we are repeating a lot of multiplication in doing this. In working out
we see that we are repeating the multiplication of 1*2*3 before multiplying the result
by 4
l1t2*31*4
and this suggests that we can produce the table in a different way, by saying how to
start
fac 0 = 1 (fac . 1)
n fac n
0 1
and then by saying how to go from one line to the next
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 (f ac.1) and (fac.2).
60 Designing and writing programs
The tablc is just a written form of the factorial function, so we can see that (f a c . 1)
and (f a c . 2) actually describe the function to calculale the faclorial, and putting [hem
together wc get
f a c : : I n t -> I n t
fac n
I n==O = 1
I n>O = f a c (n-1) * n
can be seen as generating the table of factorials, starting from f a c 0 and working up
to f a c 1, f a c 2 and so forth, up to any value we wish.
We can also read the definition in a calculational way, and see recursion justified in
another way. Take the example of f a c 4
fac 4
-w f a c 3 * 4
-
-A ( ( f a c I * 2) * 3) * 4
( ( ( f a c 0 * 1) * 2) * 3) * 4
Recursion 61
Now, we have got down to the simplest case (or basecase), which is solved by (f ac . I ) .
In the calculation we have worked from the goal back down to the base case, using the
recursion step (f a c . 2 ) . We can again see that we get the result we want. because the
recursion step takes us from n niorc 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 o f f a c two explanations for why rccursion works.
The bottom-up explanation says that the f a c equations can be seen to generate the
values o f f a c one-by-one from the base case at zero.
A top-down view starts with a goal to be evaluated, and shows how the equations
simplify this until we hit the base case.
The two views here are related, Gnce we can think of the top-down explanntion
generating a table too, but in this case the table is generated a\ it is necdcd. Starting
with the goal of f a c 4 we require the lines fix 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 t o start
to find recursive definitions. Bct'ore we d o that, we discuss another aspect of thc f a c
function as defined here.
Program e r r o r : { f a c ( - 2 ) )
because f a c is not defined on the negative numbers. We could if we wished extend the
definition to zero, on the negative numbers, thus
fac n
I n==O = 1
I n>O = f a c (n-I) * n
I otherwise = 0
( Exercises 1
4.5 Define the function rangeproduct which when given natural numbers m and n
returns the product
You should include in your definition the type of the function, and your function
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.
What if we were given the value f u n (n-I). How could we define f u n n from it?
We see how this form of recursion works in practice by looking at some examples.
1 . Suppose first that we are asked to define the function to give us powers of two for
natural numbers
power2 : : I n t -> I n t
Primitive recursion in practice 63
In fact this pattern works for any function f of type I n t -> I n t in the place of f a c ,
so we can say
sumFun : : ( I n t -> I n t ) -> I n t -> I n t
sumFun f n
I n==O = f 0
I n>O = sumFunf (n-I) + f n
where the function whose values are being added is itself an argument of the sumFun
function. A sample calculation using sumFun is
sumFun f a c 3
--i sumFun f a c 2 + f a c 3
- sumFun f a c 1 + f a c
sumFunfac 0 + f a c
facO+fac 1 + f a
2 + fac 3
I + fac 2 + f a c 3
c 2 + f a c 3
-.a ...
?-, 10
and we can define sumFacs from sumFun thus:
sumFacs n = sumFun f a c n
We briefly introduced the idea of functions as data in Chapter 1, and we will revisit
it in detail in Chapter 9. As we mentioned in Chapter I , having functions as argument\
is powerful and sumFun gives a good example: one definition serves to sum the values
of any function of type I n t -> I n t over the range of arguments from 0 to n.
64 Designing and writing programs
We will get the most new regions if we cross each of thesc 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.
regions : : Int -> Int
regions n
I n==O = 1
I n>O = regions (n-1) + n
f Exercises 1
4.7 Using the addition function over the natural numbers, give a recursive definition
of multiplication of natural numbers.
4.8 The integer square root of a positive integer n is the largest integer whose square
is less than or equal ton. For instance, the integer square roots of 15 and 16 are
3 and 4, respectively. Give a primitive recursive definition of this function.
4.9 Given a function f of type Int -> Int give a recursive definition of a function
of type Int -> Int which on input n returns the maximum of the values f 0,
f I, . . . , 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 o f f thus:
4.10 Given a function f of type Int -> Int give a recursive definition of a function
of type Int -> Boo1 which on input n returns True if one or more of the values
f 0, f I, . . ., f n is zero and False otherwise.
4.1 1 Can you give a definition of regions which instead of being recursive uses the
function sumFun'?
4.1 2 [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.
1. The sequence of Fibonacci numbers starts with 0 and I,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
fib :: Int -> Int
fib n
I n==O = 0
I n==l = I
I n>l = f i b (n-2) + f i b (n-1)
where we see in the general case that fib n depends upon not only f i b (n-I) but
also f i b (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 f i b n
requires us to calculate both f i b (n-2) and f i b (n-I),and in calculating f i b (n-1)
we will have to calculate f i b (n-2) again. We look at ways of overcoming 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
66 Designing and writing programs
-
--t
---+
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
d i v i d e 4 (-4)
-& d i v i d e (4-(-4)) (-4)
-vt d i v i d e 8 (-4)
-A ...
The lesson of this example is that in general there is no guarantee that a function defined
by recursion will always terminate. We will have termination if we use primitive
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.
Program testing 67
Exercises
4.13 Give a recursive definition of a function to tind the highest common factor of
two positive integers.
4.14 Suppose we have to raise 2 to the power n. If n is even, 2*m say, then
Program testing
Just because a program is accepted by the Haskell system, it docs not mean that it
necessarily does what it should. How can we be sure that a program behaves as it is
intended to? One option, tirst aired in Section I . 10, is to prove in some way that it
behaves correctly. Proof is, however, an expensive business, and we can get a good
deal of assurance that our programs behavc correctly by testing the program on selected
inputs. The art of testing is then to choose the inputs to be as comprehensive as possible.
That is, we want to test data to represent all the different 'kinds' of input that can be
presented to the function.
How might we choose test data? There are two possible approaches. We could
simply be told the specification of the function, and devise test data according to that.
This is called black box testing, as we cannot see into the box which contains the
function. 011the other hand, in devising white box tests we can use the form of thc
function definition itself to guide our choice of test data. We will explore these two
in turn, by addressing the example of the function which is to return the maximum of
three integers,
maxThree : : I n t -> Int -> Int -> Int
What are the testing groups for the example of maxThreed?There is not a single right
answer to this, but we can think about what is likely to be relevant to the problem and
what is likely to be irrelevant. In the case of maxThree it is reasonable to think that thc
s i ~ or
e sign of the integers will not be relevant: what will determine the result is their
relative ordering. We can make a first subdivision this way
all three values different;
all three values the same;
two items equal, the third different. In fact, this represents two cases
- two values equal to the maximum, one other;
- one value equal to the maximum, two others.
We can then pick a set of test data thus
If we test our definition in Section 3.4 with these data then we see that the program
gives the right results. S o too does the following program:
mysteryMax : : Int -> Int -> Int -> Int
mysteryMax x y z
I x>y&&x>z = X
l y > x & & y > z = Y
I otherwise = z
so should we conclude that mysteryMax computes the maximum of the threc inputs'?
If we do, we are wrong, for we have that
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;
all three values the same: one ordering;
two items equal, the third different. In each of the two cases we consider three
orderi ngs.
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.
Program testing 69
We take up the ideas discussed in this section when we discuss proof in Chapter 8.
Exercises
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.18 Test the following function
using the test data written in the previous question. What do you concludc on
the basis of your results'?
4.19 Devise test data for a function
which returns how many of its three integer inputs are larger than their average
value.
4.20 Devise test data for a function to raise two to a positive integer power.
70 Designing and writing programs
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 already
defined a function f we can make use of it in two ways.
- We can /nodel our new definition on the definition of f .
- We can use f i n our new definition.
We should think about how to break the problem into smaller, more easily solved,
parts. We should ask WIl~itif1 Izud ... ?.
We can use recursion to define functions.
We also explained the basics of recursion, and caw how i t is used in practice to define
a variety of functions. We shall see many more illustrations of this when we look at
recursion over Ii\tc 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.
( Chapter 5 >
Data types: tuples and
lists
Introducing tuples, lists and strings
Tuple types
Our approach to lists
Lists in Haskell
List comprehensions
A library database
Generic functions: polymorphism
Haskell list functions in Prelude .hs
The S t r i n g type
Thus far we have looked at programs which work over the basic types such as Int, Float
and Bool, and we have also seen how to approach the design of programs in general.
However, in practical problems we will want to represent more complex things, as we
saw with our P i c t u r e example in Chapter 1.
This chapter introduces two ways of building compound data in the Haskell language;
these are the t u p l e and the list, and together they suffice 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.
We concentrate here on explaining the facilities that Haskell provides for defining and
manipulating tuples and lists. The repertoire for tuples is small, but for lists the langauge
provides many predefined functions and operations. As well as these we can use the 'list
comprehension' notation to write down descriptions of how lists may be formed from
other lists.
72 Data types: tuples and lists
Both tuples and lists are built up by combining a number of pieces of data into a single
object, but they have different properties. In a tuple, we combine a predetermined
number of values of predetermined types - which might be different - into a single
object. In a list we combine an arbitrary number of values -all of the same type - into
a single object.
An example can help to clarify thedifference. Suppose we are trying 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. A given item has a name and a price (in pence), and we
therefore need somehow to combine thew two pieces of information. We do this in a
tuple, such as
( " S a l t : 1kgn,139)
("Plain crispsM,25)
where in each t u p l e a s t r i n g i s combined with an I n t . The literal S t r i n g of characters.
written between double quotes, gives the name of the item, and the I n t gives its price.
The S t r i n g is in fact a list of characters, and we discuss that type in Section 5.9.
The values ( " S a l t : lkg" ,139) and ( " P l a i n c r i s p s " ,25) belong to the tuple
tyPe
(String, I n t )
Every member of this type will have two components - a S t r i n g and an I n t - as
specified in the type ( S t r i n g , I n t ) . If we are given a member of this type we can
therefore predict what type its components will have, and this means that we can check
that these components are used in an appropriate way: we can check that we deal with
the second half as an I n t and not a Bool, for example. We therefore keep the property.
first mentioned in Chapter I, that we can type-check a11 programs prior to execution,
and so any type errors in a program can be found before a program is actually executed.
How are the contents of the basket represented'? We know that we have a collection
of items. but we d o not know in advance how many we have; one basket might contain
ten items, another one three. Each item is represented in the same way, as a member
of the ( S t r i n g , I n t ) type, and so we represent the contents of the basket by a list of
these, as in the list
[ ("Salt: lkgH,139) , ( " P l a i n c r i s p s 8 ' , 2 5 ) , ("Gin: l l t " , 1 0 9 9 ) 1
This is a member of the list type
Tuple types 73
Other members of this list type include the empty list, [I, and the basket abovc with a
second packet of crisps replacing the gin:
t y p e ShopItem = (String,Int)
t y p e Basket = CShopItem]
where thc keyword t y p e introduces the fact that this is the detinition of a typc rnther
than a value. We can also tell this because the type names ShopItem and Basket begin
with capital lctters. as noted i n Section 3.7. Built into the system is thc definition
type S t r i n g = [Char]
so Haskell treats strings as a special case of the list type. Names such as ShopItem and
S t r i n g are synonyms for the types which they name.
We now look at tuple types in more detail, and examine some examples of how tuples
are used in practice.
Tuple types
The last section introduced the idea of tuple types. In general a tuple lype is built up
from components of simpler types. The type
t y p e ShopItem = ( S t r i n g , I n t )
74 Data types: tuples and lists
and we saw above that its members include items like ("Gin, lltH,1099).
A type definition like this is treated as shorthand in Haskell - wherever a name
like ShopItem is used, it has exactly the same effect as if (String,Int) had been
written. Definitions like this make programs more readable and also lead to more
conlprehensible type error messages.
How else are tuple types used in programs? We look at a series of examples now.
1 . First, we can use a tuple to return a compound result from a function, as in thc
example where we are required to return both the minimum and the maximum of two
Ints
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 tuple\. Functions over tuples
are usually defined by pattern matching. Instead of writing a variable for an argument
of type (Int ,Int) , say, a pattern, (x ,y) is used.
On application the components of the pattern are matched by the corresponding cotn-
ponents of the argument. so that on applying the function addpair to the argument
( 5 , 8 ) the value 5 is matched to x, and 8 to y, giving the calculation
Tuple types 75
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 : : (~nt,Int)-> Int
addpair p = f st p + snd p
but generally a pattern-matching definition is easier to read than one which usov selector
functions instead.
( Examples )
in Section 4.4, 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
as a result. In other words we want t o define a function f ibPair
two c.ortsecutive \?c~lue.s
so that it has the property that
fibpair n = (fib n , fib (n+l))
then given such a pair, (u ,v) we get the next pair as (v ,u+v), which is exactly the
effcct of the f ibStep function:
76 Data types: tuples and lists
fibstep 2 3 fibTwoStep ( 2 , 3 )
We say more about the relationship between these two functions in Section 10.7.
which returns the maximum of two integers. together with the number of times
it occurs. Using this, or otherwise, define the function
which puts the elements of a triple of three integers into ascending order. You
might like to use the maxThree, m i d d l e andminThree functions defined earlier.
5.3 Define the function which tinds where a straight line crosses the x-axis. You
will need to think about how to supply the information about the straight line to
thc 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.
(Lists in Haskell
A list in Haskell is a collection of items from a given type. For every type t there is a
Haskell type [tl of lists from t.
[1,2,3,4,1,41 : : [Intl
[True] : : [Bool]
We read these as ' [1,2,3,4,1,4] is a list of Int' and '[True] is a list of Bool'.
String is a synonym for [Char] and the two lists which follow are the same.
['a', 'a', 'b'] : : String
"aab" : : String
We can build lists of items of any particular type, and so we can have lists of functions
and lists of lists of numbers, as in
[fac,fastFib] : : [ Int -> Int 1
: : C [Intl I
[~12,21,~2,121,~11
As can be seen, the list with elements el,e2 to en is written by enclosing the elements
in square brackets, thus
As a special case the empty list, [I, 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:
The first two have length 5, while the third has length 4; the first element of the first
list is I, while the first element of thc 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
[n .. ml is the list [n,n+l,. . . ,ml; if n exceeds m, the list is empty.
[2 ..
71 = [2,3,4,5,6,71
13.1 . . 7.01 = [3.1,4.1,5.1,6.11
['a' . . 'm'] = "abcdefghijklm"
[n,p . . ml is the list of numbers whose tirst two elements are n and p and whose
last is m, with the numbers ascending in steps of p-n. For example,
[7,6 . . 31 = [7,6,5,4,31
[0.0,0.3 . . 1.01 = [0.0,0.3,0.6,0.91
['a', 'c' . . 'n'] = "acegikm"
List comprehensions 79
In both cases i t can be seen that if the step size does not allow us to reach m exactly,
the last item of the list is the largestlsmallest in the sequence which is lesslgreater
than or equal to m. It can also be the case that rounding errors on F l o a t 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.
( Exercises ]
5.5 What value has the expression [O , 0.1 . . 11 ? Check your answer in Hugs
and explain any discrepancy there might be between the two.
5.6 How many items does the list [2,3]contain? How many does [ [2,3]1 contain?
What is the type of [ [2,31I ?
(5.5)List comprehensions
One of the distinct features of a functional language is the list comprehension notation,
which has no parallels i n other paradigms.
In a list comprehension we write down a description of a list in terms of the elements
of another list. From the first list we generate elements. which we test and transform
to form elements of the result. We will describe list comprehensions with a single
generator in this section; Section 17.3 covers the general case. Nevertheless, the simple
case we look at here is very useful in writing a variety of list-processing programs. We
introduce the topic by a series of examples.
[ 2*n I n<-ex]
will be
[4,8,l4l
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, 'E'. We can write the evaluation of the list comprehension in a table, thus:
80 Data types: tuples and lists
2. In a similar way,
[ isEven n I n<-ex 1 -- [True,True,False]
if the function isEven has the definition
isEven : : I n t -> Boo1
isEven n = (n 'mod' 2 == 0)
In list comprehensions n<-ex is called a generator because it generates the data from
which the results are built. On the left-hand side of the '<-' there is a variable, n,while
on the right-hand side we put the list. in this case ex,from which the elements are taken.
3. We can conlbine a generator with one or more tests, which are Boolean expressions,
thus:
[ 2*n I n <- ex , isEven n , n>3 1 (2)
(2) is paraphrased as
'Take all 2*n where n comes from ex,n is even and greater than 3.'
Again, we can write the evaluation in tabular form.
[ 2*n I n <- [2,4,7] , isEven n , n>3 1
n = 2 4 7
isEven n = T T F
1
-
0
3 = F T
2*n = 8
The result of (2) will therefore be the list [8l, as 4 is the only even element of [2 $4$71
which is greater than 3.
4. Instead of placing a variable to the left of the arrow '<-', we can put a pattern. For
instance,
Here we choose all the pairs in the list pairlist,and add their components to give a
single number in the result list. For example,
C m+n I (m,d <- [(2,3),(2,l) ,(7,8)l I
List comprehensions 81
addOrdPairs : : [ ( I n t ,I n t ) 1 -> [ I n t ]
addOrdPairs p a i r L i s t = [ m+n I (m,n) <- p a i r L i s t , m<n 1
so that with the same input example,
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
d i g i t s : : S t r i n g -> S t r i n g
d i g i t s s t = [ ch I ch<-st , i s D i g i t ch 1
where the prelude function
is True on those characters which are digits: ' 0 ' , ' 1' up to ' 9 ' .
7. A list comprehension can form a part of a larger function definition. Suppose that
we want to check whether all members of a list of integers are even, or all are odd. We
can write
We will see list comprehensions in practice in the next section when we examine a
simple library database.
82 Data types: tuples and lists
which converts all small letters i n 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? You should check the Char .hs library to see whether it contains any
functions useful in solving this problem.
5.10 Define the function
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).
5.1 1 Define the function
which is True if the Int is an element of the list, and False otherwise. For the
examples above, we have
(A library database
This section presents a simple model of the loan data kept by a library, and illustrates
how list comprehensions are used in practice.
A library uses a database to keep a record of the books on loan to borrowers; we first
look at which type to use to model the database, and then look at the functions which
extract information from a database. This is followed by a discussion of how to model
changes to the database, and we conclude by exploring how the database functions can
be tested.
TYpes
In modelling this situation, we first look at the types of the objects involved. People
and books are represented by strings
The database can be represented in a number of different ways. Three among a number
of possibilities are
Here we choose to make the database a list of (Person,Book) pairs. If the pair
("Alice" , "Asterix") is in the list, it means hat "Alice" has borrowed the book
called "Asterix". We therefore define
exampleBase :: Database
exampleBase
= [ ("Alice" , "Tintin") , ("Anna" , " L i t t l e Women") ,
("Alice" , " A s t e r i x " ) , ("Rory" , " T i n t i n " ) ]
After defining the types of the objects involved, we consider the functions which work
over the database.
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
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 being
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 di/]hrent database, which
is the original with the loan added or removed. These update functions will have type
which forms a model for the other lookup functions. For the exampleBase, we have
How are thew foundi? In the "Alice" case we need to run through the list exampleBase
tinding all the pairs whose first component is "Alice"; for each of these we return the
second component. As a list comprehension, we have
A library database 85
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 t o join two lists, namely the one element list
[ (pers,bk) I 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 i n 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 Hugs prompt. For example,
makeLoan [I "Alice" "Rotten Romans"
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
testl : : Boo1
testl = 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 $$, which is short for 'the last exprcssion evaluated'. The following sequence
makes a loan, then another, then returns the tirst.
makeLoan exampleBase "Alice" "Rotten Romans"
makeLoan $$ "Rory" "Godzilla"
returnLoan $$ "Alice" "Rotten Romans"
The effect of this is to return all the books borrowed by d l borrowers, not just the
particular borrower f indperson.
The reason for this is that the f indPerson in (findperson,book) is a new
variable, and not the variable on the left-hand side of the definition, so in fact
(books.2) has the same effect as
f Exercises 1
5.1 2 Go through the calculation of
Generic functions: polymorphism 87
5.1 5 Discuss how you would implement the database functions had you used the repre-
sentation [ (Person, [Book] I rather than [(Person ,Book)I for the database.
and so forth. How do we write down a type for length which encapsulates this? We
say
where a is a type variable. Any identifier beginning with a small letter can be used as
a type variable; conventionally, letters from the beginning of the alphabet, a, b, c, . . .
are used. Just as in the definition
square x = x*x
the variable x stands for an arbitrary value. so a type variable stands for an a/-birr-crr;~
tvpe, and so we can see all the types like
The variable a stands for 'an arbitrary type', but we should be clear that all the a's stand
for the same type, just as in
square x = x*x
the x's all stand for the same (arbitrary) value. Instances of [a] -> [a] -> [a] will
include
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 z i p and u n z i p convert between pairs of lists and
lists of pairs, and their types involve two type variables:
where a and b are replaced by different types ( I n t and Bool, here). It is, of course,
possible to replace both variables by the same type, giving
which returns its argument unchanged. In the definition there is nothing to constrain
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
At work here is the principle that a function's type is as general as possible, consistent
with the constraints put upon the types by its definition. In the case of the i d function,
the only constraint is that the input and output types are the same.
In a similar way, in defining
Generic functions: polymorphism 89
f s t (x,y) = x
neither x nor y is at all constrained, and so they can come from different types a and b.
giving the type
f s t : : (a,b) -> a
Wc >hallexamine the definitions of many of the prelude functions in Chapter 7. and see
there that, as outlined above, a function or other object will have as general as possible
a type, consistent with the constraints put upon the types by its definition. We look i n
more depth at the mechanics of type checking in Chapter 13.
Hugs can be used to give the most general type of a function definition, using the
: t y p e command. If you have given a type declaration for the function. this can be
commented out before asking for the type.
f s t (x,y) = x
( Exercises 1
5.16 Give the most general types for the functions s n d and s i n g defined by
snd ( x , y ) = y
sing x = [XI
90 Data types: tuples and lists
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 5.1, the standard prelude provides
various operations over specific types; some of these can be seen in Figure 5.2. The
types of the functions sum and product,which are overloaded, will be discussed further
in Chapter 12.
Looking at Figure 5.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.
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.
Haskell list functions in Prelude.h s 91
concat [[all -> [a1 Concalenale a list of lists into a single list.
concat [ [2,31 , [I , [411 -.> [2,3,41
tai1,init [a] -> [a] All but the firstllast element of the list.
t a i l "word" -a "ord"
take I n t -> [a] -> [a] Take n elements from the front of a list.
t a k e 3 "Peccary" -i l1Pecl1
drop I n t -> [a] -> [a] Drop n elements from the front of a list.
drop 3 "Peccary" --i " c a r y "
splitAt I n t -> [a] -> ( [a] , [a] ) Splil a lisl a1 a given position.
s p l i t A t 3 "Peccary" --. ( " P e c " , " c a r y " )
zip [a1 -> [bl-> [ ( a , b) 1 Take a pair of lists into a list of pairs.
z i p [1,21 [ 3 , 4 , 5 1 -.a [ ( 1 , 3 ) , (2,411
unzip [ ( a , b > l -> ([a] , [ b l ) Take a list of pairs into a pair of lists.
unzip [ ( l , 5 ) , (3,611 --t ( [ 1 , 3 1 , [5,61)
product
Figure 5.2
[ I n t l -> I n t
[ F l o a t ] -> F l o a t
The product of a numeric list.
p r o d u c t [O. 1,O . 4
Further functions
Wc havc 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 9; secondly, some of the functions, such as z i p 3 , are obvious variants
of things we have discussed here. Similarly, we havc not chosen to enumerate the
functions in the library L i s t . h s ; readers should consult the library file itself. which
contains type information and comments about the effects of the functions.
In the next chapter we explore how to use the prelude functions in making our own
definitions of functions; before that we discuss strings, an examplc of a list type.
t y p e S t r i n g = [Char]
and all the polymorphic prelude functions in Figure 5.1 can be used over strings. I n
Section 3.5 we showed how to write the special characters such as newline and tab
using the 'escapes7 ' \n ' and ' \ t ' . These characters can form part of strings, as in the
examples
"baboon"
I1 I 1
I f we evaluate one of these strings in Hug\, the result is exactly the same as the input. In
order to resolve the escape characters and to loce the double quotes we have to perl'orm
an output operation. This is done using the primitive Haskell function
p u t S t r : : S t r i n g -> I 0 0
The S t r i n g type 93
with the effect of putting the argument string on the screen. Applying p u t S t r to the
strings above gives output as follows:
baboon
cat
gorilla
hippo
ibex
1 23 456
Strings can be joined together using ++, so that "catl'++"\n"++"fi s h " prints as
cat
fish
Note: Names, strings and characters
It is easy to confuse a, ' a ' and "a". To summarize the diffcrcnce.
a is a namc or a variable, if defined it may have any typc
whatever:
'a' is a character;
#I a I! is a ~tring.which just happens t o consist o f a single
character.
Similarly, there is n difference between
emu a Haskell name or variable:
"emu" a string.
show (True I I F a l s e )
-
value t o a S t r i n g and vice versa; for instance.
show (2+3)
--,
"5"
"True"
In the opposite direction, the function r e a d is used to convert a string to the value i t
represents, so that
read "True"
read "3"
- True
3
In some situations it will not be clear what should be the result type for r e a d - it is
then possible to givc a type to the application, as in
(read "3") : : I n t
the result of which will be 3 and its type. I n t .
A full explanation of the types of read and show can be found in Chapter 12.
94 Data types: tuples and lists
5.19 Define a function to convert small letters to capitals which returns unchanged
characters which are not snmll letters.
5.20 Define a function
which converts a digit to its repmentation in Roman numerals, so at ' 7 ' it will
have the value "VII" and so on.
5.21 Define a function
which takes three strings and returns a single string which when printed shows
the three strings on separate lines.
5.22 Define a function
which takes a list of strings and returns a single string which when printed shows
the strings on separate lines.
5.23 Give a function
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, " I 1 ,
and if n is I, the result will be the string itself.
5.24 Give a function
which takes a string and forms a string of length linelength by putting spaces at
the front o f t h e 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 critici~ethe 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.
5.26 Define a function
n fib n
0 0
1 1
2 1
3 2
4 3
5 5
6 8
5.27 Define functions to give more readable output from the database operations of
Section 5.6.
( Summary
This chapter has introduced the structured types of tuples and lists, and explained their
differences: in a given tuple type, ( t 1 , . . .t n ) the elements all have the same form.
namely (vl , . . . v n ) , with each component v i being a member of the corresponding
type t i . The list type [tl on the other hand contains elements [el , . . . ,en1 of
different lengths but in which all the values e i have the same type t .
Over tuples we introduced the notion of pattern matching - in which a pattern S L K ~
as (x ,y ) could bc used to stand for an arbitrary member of a pair type - and saw how
this led to more readable definitions.
The bulk of the chapter was an account of the fxilities which Haskell provides for
working with lists. These include
various ways of writing lists of elements of base type, including ranges like [ 2 , 4 . . 121 ;
list comprehensions, in which the members of a list are generated, tested and trans-
formed from the elements of another list, as exemplified by
which selects the alphabetic characters from s t r i n g , and converts them to upper
case;
the functions provided by the standard prelude and the L i s t . h s library;
S t r i n g as the list type [Char] .
In order to understand the prelude functions it was necessary to discuss polymorphism,
by which a function can have 'many types'. Types of functions like this are described
by using type variables, as in
r e v e r s e : : [a1 -> [a1
which states that r e v e r s e can be applied to a list of any type ( a is a type variable).
returning a member of the same list type.
In the chapters to come we will use the list functions given here in making our own
definitions, as well as seeing how the prelude and library functions are themselves
defined.
( 1
Chapter 6
We revisit and extend the Picture example in order to illustrate some of the ideas
which we introduced in the previous chapter.
We discuss the mechanism for making definitions local to a function or expression.
This becomes important when we start to write more substantial programs, as it makes
them both more readable and potentially more efficient.
We introduce two extended sets of exercises to stretch the reader rather more than
the small exercises we have given thus far. The two case studies are
- an extension of Pictures to give them a position (in space); 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.
The chapter following this discusses how we implement the primitive functions over lists
using recursion; readers may skip forward to this, reading only Sections 6.1 and 6.3 on
Pictures and local definitions.
In this section we revisit the Picture example, first introduced in Chapter I and rc-
examined in Section 2.5. What we do here is to look at how to implement some o f the
operations over the Picture type
The Picture example, revisited 97
Some of the operations are defined as library functions. To flip a picture in a hori~ontal
mirror, we sinlply have to reverse the order of the lines of the picture:
and to place one picture above another it is sufficient to join the two lists of lines
together:
and we can read off from this program its intended effect:
We shall see that this operation is itself a higher-order function in Chapter 9 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 14. How can
we accomplish this? We can see this as like f lipV, in that we want to do something 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
sideBySide will clip the bottom lines off whichever picture is the longer; if they arc
the same length, then there is no clipping. We can also use the higher-order zipwith
to define sideBySide; we revisit this in Chapter 9.
In our pictures, white is represented by the dot ' . ' and black by the hash sy~nbol'#'.
To invert the colour of a single character we define
98 Programming with lists
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
invertline : : [Char] -> [Char]
invertLine line
= [ invertchar ch I ch <- line 1
and we want to apply this to all the lines in the picture
invertcolour : : Picture -> Picture
invertcolour pic
= [ invertLine line I line <- pic 1
We could if we wish write this as a single definition, thus
invertcolour : : Picture -> Picture
invertcolour pic
= [ [ invertchar ch 1 ch <- line ] I line <- pic 1
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.
( Exercises 1
6.1 Detine a function
so that the superimposition of ' .' with itself gives ' .' while any other combi-
nation of characters gives '#'.
6.2 Define a function
which takes two lines - which you can assume are of the same length - and
superimposes their corresponding characters using superimposechar,so that,
for example,
which superimposes two pictures, which you may assume have the same dinien-
sions.
6.4 Using the function putStr : : String -> 10 0 and any other functions you
might need, define the function
so that the effect ofprintpicture [ " .##. " , " .# . # " , ".
###I1 , It####" I
is that
where True and False represent black and white points in a picture. How would
you have to modify the functions working over Picture to accommodate this
change'? What are the advantages and disadvantages of the two represcntations'?
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 i t h elements
in each of the lines of the original picture, reflected in a hori~ontalmirror.
6.7 Using rotate90 or otherwise, define a function which rotates a picture through
90" anticlockwise.
100 Programming with lists
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 picture.
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,
We will use the term Image for a picture with a position, and so we define
An example, in which we position the horse with its bottom left-hand corner or
reference point at position (31,23), is given in Figure 6.1.
The remainder of this section is a collection of exercises to write functions which
manipulate these Images;you can use any of the list functions introduced in the previous
chapter and also the functions over Picture which we have already defined.
which takes an Image and returns a new Image whose Picture is unchanged
but whose Posit ion is given by the second argument to changeposit ion.
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 P i c t u r e 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.
If we think of reflections and rotations going on in space, then the results are lnorc
likely to be as shown in Figure 6.2, where we see that the position of the resulting 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 operations will change the
reference point. We call this the geometrical view of the transformations.
( Exercises >
6.1 3 Implement for Image the analogues o f f lipH, f lipV, r o t a t e and r o t a t e 9 0
under the naive view of how to lift the transformations.
6.14 Implement for Image the analogues of flipH, f lipV, r o t a t e and r o t a t e 9 0
under the geometrical view.
Superimposition
When pictures have positions, superimposition can be more complex. Considcr the
example illustrated in Figure 6.3; here we see one way of superimposing the two images
is to use P i c t u r e superimposition on two pictures which have first been 'padded out'
with white space as shown in the figure.
Exercises
6.16 Using the padding functions, define a superimposition function for the Image
tY Pe.
6.17 How would you use Image superimposition to give analogues of above and
sideBySide for Images?
( Examples
1. A simple example is given by a function which is to return the sum of thc squares
of two numbers.
The result of the function will be the sum of two values, s q N and sqM, so that
104 Programming with lists
The definition of these two values can be done in the where clause which follows the
equation, thus
sumsquares n m
= sqN + sqM
where
sqN = n*n
sqM = m*m
In such a simple example, it is perhaps hard to see the point of making the local
definitions, but in practice many situations occur when a local definition or definitions
make a solution both more readable and more efficient. We look at such an example
now.
which adds corresponding elements of the two lists, dropping any elements which fail
to have a 'partner'. For instance,
Now suppose that we are asked to make sure that any elements without a partner are
added to the end of the list. This will be the function
How can we approach this problem'? Using the functions take and drop first introduced
in Figure 5.1 we are able to split up the argument lists.
If minLength is the minimum of the two list lengths, then the front part of the result
is given by
at least otw of ~ h i c h [I, and so we can collect all the elements 'without partners'
will he
simply by joining these two lists together. The function we want can therefore bc L'71ven
by the definition
addpairwise' i n t L i s t l i n t L i s t 2
= f r o n t ++ r e a r
where
minLength = min ( l e n g t h i n t L i s t l ) ( l e n g t h i n t L i s t 2 )
front = addpairwise ( t a k e minLength i n t L i s t l )
( t a k e minLength i n t L i s t 2 )
rear = drop minLength i n t L i s t l ++ drop minLength i n t L i s t 2
Now, we have a gain in efficiency becauce minLength will only be calculated once,
even though it is used four times in the def nition. We also have a definition which i h
easier to read: we cee that the result has two parts, f r o n t and r e a r , and we can read
their definitions separately.
We can, in fact, make a further efficiency gain, by replacing separate calls lo t a k e
and drop by a single call to s p l i t A t , also introduced in Figure 5.1. as in
addpairwise' i n t L i s t 1 i n t L i s t 2
= f r o n t ++ r e a r
where
minLength = min ( l e n g t h i n t L i s t l ) ( l e n g t h i n t L i s t 2 )
front = addpairwise f r o n t 1 f r o n t 2
rear = r e a r 1 ++ r e a r 2
(f r o n t l , r e a r l ) = s p l i t A t minLength i n t L i s t 1
( f r o n t 2 , r e a r 2 ) = s p l i t A t minLength i n t L i s t 2
In this example we see a third use of a where clause. We can put a pattern - like
(f r o n t l ,r e a r l ) -on the left-hand side of a definition; the result of this is to associate
the names f r o n t l and r e a r l with the corresponding components of the expression on
the right-hand side - in this case the split of the list i n t L i s t l .
A pattern match of this form is called conformal, as the expression on the right-hand
side of the definition has to conform to the pattern on the left, otherwise the delinition
fails.
Another important point in this example is that the order of different definitions is
irrelevant. In particular it is possible to use a value before it is defined: the definilions
of f r o n t and r e a r precede those of f r o n t l and r e a r l which they use. This is
equally true for scripts in general, in which the order of the top-level definitions is
irrelevant.
Layout
In definitions with where clauhcs, 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
106 Programming with lists
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
I gi = el
...
I otherwise = er
where
VI a1 . . . an = rl
v2 = 1-2
....
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 vl 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.
l e t expressions
It is also possible to make definitions local to an expression. For instance, we can write
let x = 3+2 in x-2 + 2*x - 4
giving the result 31. If more than one definition is included in one line they need t o be
separated by semi-colons, thus:
let x = 3+2 ; y = 5-1 in x-2 + 2*x - y
We shall find that we use this form only occasionally.
Scopes
A Haskell script consists of a sequence of definitions. The scope of a definition i~ 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, isEven : : Int -> Boo1
isOdd n
1 n<=O = False
I otherwise = isEven (n-I)
Local definitions 107
isEven n
I n<O = False
I n==O = True
I 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 appear. 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 detinitions 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
otherwise = SqY
where
sqx = sq x
SqY = sq Y
sq : : Int -> Int
sq z = z*zl
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 exanlple below, the variable x appears twice. Which definition is in force at each
point? The mo.rt local is the one which is used.
maxsq x y
1 where
I
In the example, we can think of the inner box cuttirzg a Izole in the outer, so that the
scope of the outer x will exclude the definition of sq. When one definition is contained
108 Programming with lists
inside another the best advice is that different variables and names should be used for
the inner detinitions 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 namc at
the same level; one of them needs to be hidden if a clash occurs due to the combination
of a number of niodules.
Calculation
The way in which calculations are written can be extended to deal with where clai~ses.
The sumsquares function in the previous section gives. for example
sumsquares 4 3
= sqN + sqM
where
sqN = 4*4 = 16
sqM = 3*3 = 9
= 1 6 + 9
= 25
The values of the local definitions arc calculated beneath the where if their values are
needed. All local evaluation below the where is indented. To follow the top-levcl valuc.
we just have to look a1 the calculation at the left-hand side.
The vertical lines which appear are used to link the successive steps of a calculation
when these have intermediale where calculations. The lines can be omitted.
f Exercises
6.18 Detine the function
maxThreeOccurs : : I n t -> I n t -> I n t -> ( ~ n t1,n t )
which returns the maximum of three integers paired with the number of tinies
it occurs among the three. A natural solution first tinds the maxin~um.and then
invcstigates how often it occurs among the three. Discuss how you would write
your s o l ~ ~ t i oif'nyou were not allowed to use where-definitions.
6.19 Give sample calculations of
comprehensions and also lhe prelude functions mentioned here. We will a l w expect
local definitions as explained in Section 6.3 to be used when appropriate.
- -
The problem
A scanner at a supermarket checkout will produce from a basket of shopping a list of
bar codes, like
Dry S h e r r y , l l t . . . . . . . . . . . 5.40
Fish Fingers . . . . . . . . . . . . . . 1.21
Orange J e l l y . . . . . . . . . . . . . . 0.56
Hula Hoops (Giant) . . . . . . . . 1 . 3 3
Unknown Item . . . . . . . . . . . . . . 0.00
Dry S h e r r y , llt . . . . . . . . . . . 5.40
T o t a l . . . . . . . . . . . . . . . . . . . . 13.90
We havc to decide first how to model the objects involved. Bar ccnlc.3 and prices (in
pence) can be modelled by integers; names of goods by strings. Wc say 111crcti)rcthat
t y p e Name = String
type Price = Int
t y p e BarCode = I n t
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.
t y p e Database = [ (Barcode ,Name , P r i c e ) 1
The example database we use is
codeIndex : : Database
codeIndex = [ (4719, " F i s h Fingers" , 1211,
(5643, "Nappies" , 1010),
(3814, "Orange J e l l y " , 561,
(1111, "Hula Hoops", 211,
(1112, "Hula Hoops (Giant) " , 1331,
(1234, "Dry S h e r r y , l l t " , 54011
The ob.ject of the script will be to convert a list of bar codes into a list of (Name , P r i c e )
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)]
110 Programming with lists
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 namelprice pairs,
formatBill : : BillType -> String
which takes a list of namelprice pairs into a formatted bill, and
produceBill : : TillType -> String
which will combine the effects of makeBill and f ormatBill, thus
The length of a line in the bill is decided to be 30. This is made a constant, thus
lineLength : : Int
1ineLength = 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.
f Exercises 1
6.20 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 function,
define a function
Recall that I \n I 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,
which uses look to look up an iteni in the particular database codeIndex. This
function clashes with a function lookup defined in the prelude: consult page 4 1
for details of how to handle this.
which applies lookup to every iteni in the input lict. For instance, when
applied to [1234,4719,3814,1112,1113,1234] the result will bc the list of
(Name,Price) pairs given in Exercise 6.25. Note that 11 13 does not appear in
codeIndex and $0 is converted to ("Unknown Item",0 ) .
This completes the definition of makeBill and together with f ormatBill gives
the conversion program.
6.29 You are asked to add a discount for multiple buys of sherry: for every two bottles
bought, there is a 1.00 discount. From the example list of bar codes
the bill should be as illustrated in Figure 6.4. Yo11 will probably find it helpful
to deti ne functions
Extended exercise: supermarket billing 11 3
Haskell Stores
Discount . . . . . . . . . . . . . . . . . . 1.00
Total . . . . . . . . . . . . . . . . . . . . 12.90
6.30 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.31 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 item7 pairs.
6.32 [Project] Design a script of functions to analyse collections of sales. Given a list
of TillType, produce a table showing the total sales of each item. You might
also analyse the bills to see which pairs of i t e m are bought together; this could
assist with placing items in the supermarket.
( Summary
This chapter has introduced the idea of local definitions, most importantly the where
clauses attached to the conditional equations in function definitions. We illustrated the
way in which these definitions are used to make definitions more readable, and also to
avoid re-computation of results, like the minLength in the example addpairwise'.
We gave a general template for the layout of function definitions including guards and
where clauses.
114 Programming with lists
We also saw 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 P i c t u r e example 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.
( Chapter 7 1
Defining functions over
lists
7.1 Pattern matching revisited
7.2 Lists and list patterns
7.3 Primitive recursion over lists
7.4 Finding primitive recursive definitions
7.5 General recursions over lists
7.6 Example: text processing
We have already seen how to define a variety of functions over lists using a combination of
list comprehensions and the built-in list processing functions in the Haskell prelude. This
chapter looks 'under the bonnet' and explains how functions 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 x y
I x==o = Y
I otherwise = x
where a choice of two alternatives is made by guards; we can rewrite this into two
equations, thus
mystery 0 y = y (mystery. I )
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. I) does not apply.
Another aspect of this delinition 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 argument
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 between
certain sorts of cases in function definitions. We have also seen pattern matching used
to name the components of tuples, as in
joinstrings : : (String,String) -> String
joinstrings (stl,st2) = st1 ++ "\tU ++ st2
where the variables st1 and st2 will be matched with the components of any argument.
In working with lists the two aspects of distinguishing cases and extracting compo-
nents are uscd together, as we see in the next section.
Summarizing patterns
A pattern can be one of a number of Lhings:
A literal value such as 24, 'f ' or True; an argument matches thih pattern if it is
equal to the value.
A variable such as x or 1ongVariableName;any argument value will match this.
A wildcard '-'; any argument value will match this.
A tuple pattern (pi ,p2,. . . ,pn). To match this, an argument must be of the
form (vl ,v2,. . . , v n ) , and each vk must match pk.
A constructor applied to a number of patterns; we will examine this case in the
next section and in Chapter 14 below.
In a function definition we have a number of conditional equations, each of which
will have a left-hand side in which the function is applied to a number of patterns. When
the function is applied we try to match the arguments with the patterns in sequence, and
we use the first equation which applies; pattern matching in Haskell is Lhus sequential,
in a similar way to the conditions expressed by guards.
Lists and list patterns 11 7
and we can write the list using ' : ' repeatedly thus:
4 : 2 : 3 : [I
Note that here we use the fact that ' : ' is right associative, so that for any values of x,
y and z s ,
It is also not hard to see that 4 : 2 : 3 : [I is the orlly way that [ 4 , 2 , 3 ] can be built using
' : '. The operator ' : ', of type
therefore has a special role to play for lists: it is a constructor for lists, since every list
can be built up in a unique way from [I and ' : '. For historical reasons we sometimes
call this constructor cons. Not all functions are constructors: ++ can be used to build
lists. but this construction will not be unique, since, for example
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
head : : [a] -> a
head ( x : - ) = x
where head takes the first item in a non-empty list, t a i l takes all but the head of a
non-empty list and n u l l checks whether or not a list is empty.
In the definition of n u l l 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 t a i l , then a
different pattern, ( - :x s ) , is used.
It has become an informal convention in the Haskell community to write variables
over lists i n the form xs, y s (pronounced 'exes', 'whyes') and so on, with variables
x, y, . . .ranging over their elements. We will - when using short variable names -
endeavour to stick to that convention.
We can now explain the final case of pattern matching. A constructor pattern over
lists will either be [I or will have the form (p:ps) where p and p s are themselves
patterns.
In the case of the pattern (x: x s ) , 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.
A pattern involving a constructor like ' : ' will always be parenthesized, since function
application binds more tightly than any other operation.
f i r s t D i g i t : : S t r i n g -> Char
f irstDigit st
= c a s e ( d i g i t s s t ) of
[I ->'\O'
( x : J -> X
A case expression has the effect of distinguishing between various alternatives - here
those of an empty and a non-empty list - and of extracting parts of a value, by associating
values with the variables in a pattern. In the case of matching e with (x :-) we associate
the head of e with x; as we have used a wild-card pattern in (x: J, the tail of e is not
associated with any variable.
In general, a c a s e expression has the form
Primitive recursion over lists 119
case e of
p1 -> e l
P2 -> e2
...
Pk -> ek
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.
7.3 Give solutions to the previous two questions without using pattern matching.
sum [I = 0
. ... sum [51 = 5 . ...
. . .. sum [7,51 = 12 ....
.... sum [2,7,51 = 14 ....
.... sum [3,2,7,51 = I 7 ....
and just as in the caw of factorial. we can describe the table by describing the first line
and how to go from one Iine to the next, as follows:
sum : : [ I n t l -> I n t
sum [I = 0 (sum.1)
sum (x:xs) = x + sum xs (sum.2 )
This gives a definition of sum by primitive recursion over lists. In such a detinition
we give
There is also a calculational explanation for why this form of recursion works; again,
this is just like the case put forward in Section 4.2. Consider the calculation of sum
[ 3 , 2 , 7 , 5 ] . Using the equation (sum. 2) repeatedly we have
sum [3,2,7,51
- 3 + sum C2,7,51
3 + (2 + sum [7,51)
-- 3 + (2 + (7 + sum [51)>
^Vf 3 + (2 + (7 + ( 5 + s u m [ I ) ) )
-and now we can use the equation (sum. 1) and integer arithmetic to give
--
3 + (2 + (7 + ( 5 + 0)))
17
We can see that the recursion used to define sum will give an answer on any finite list
since each recursion step takes us closer to the 'base case' where sum is applied to [I.
In the next section we look at a collection of examples of definitions by primitive
recursion.
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?
7.5 Define the functions
and, o r : : [Boo11 -> Boo1
which give the conjunction and disjunction of a list of Booleans. For instance,
and [ F a l s e , True] = F a l s e
or [ F a l s e , True] = True
On an empty list and gives True and o r gives F a l s e ; explain the reason for
these choices.
What if we were given the value f u n xs. How could we define f u n ( x : x s ) from
it?
We explore how definitions are found through n series of examples.
1. By analogy with sum, rnany other functions can be defined by 'folding in' an
operator. The prelude functions p r o d u c t , and and o r are examples; here we look at
how to define the prelude function c o n c a t ,
c o n c a t [ e l , e 2 , . . . ,en] = e l + + e 2 + + . . .++en
c o n c a t [I = [I
concat (x:xs) = ....
How do we find c o n c a t ( x : x s ) if we are given c o n c a t x s ? Look at the example
where ( x : x s ) is the list [el ,e 2 , . . . , e n ] . The value of c o n c a t x s is going to be
and the result we want is e l + + e 2 + + . . .++en, and so we simply have to join the list x
to the front of the joined lists c o n c a t x s , giving the definition
c onc a t [I = [I
concat (x:xs) = x ++ c o n c a t x s
Looking at the definition here we can see that ( x : x s ) is a list of lists, since its element
is joined to another list in ( c o n c a t . 2 ) ; the type of x will be the type of the result.
Putting these facts together we can conclude that the type of the input is [ [a] I and the
type of the output is [a] ; this agrees with the type given in ( c o n c a t . 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 x s we have
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
by being an element of ys.
It is this second case where we use the value elem x ys, and we make the following
primitive recursive definition of elem.
elem x [I = False
elem x (y:ys) = (x==y) II (elem x ys)
in which the equality check is done by repeating the variable x on the left-hand
side of (elem.3). Unfortunately, repeated variables like this are not permitted in
Haskell patterns.
i
but we could ask whether this can be done 'by hand', as it were, using primitive
recursion. Looking at some examples, we expect that
Finding primitive recursive definitions 123
so that to double all the elements of (x :xs) we need to double all the elements of xs.
and to stick 2*x on the front. Formally, we have
5. Suppose that we want to select the even elements from an integer list.
but can we give a primitive recursive definition of this function? For an empty list,
there are no elements to select from,
but what happens in the case of a non-empty list'? Consider the examples
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)
I isEven x = x : selectEven xs
1 otherwise = selectEven xs
6 . As a find example, supposc that we want to sort a list of numbers into ascending
order. One way to sort the list
It is then a matter of inserting the head, 7, in the right place in this list. to give the result
This gives the definition of iSort - the 'i' is for insertion sort.
isort [I = [I
iSort (x:xs) = ins x (iSort xs)
124 Defining functions over lists
To get some guidance about how i n s should behave, we look at some examples.
Inserting 7 into [2,3,91 was given above, while inserting 1 into the same list gives
The function can now be defined. including the case that the list is empty.
i n s x C1 = [XI ( i n s . 1)
i n s x (y:ys)
1 x<=y = x: ( y : y s ) ( i n s . 2)
1 otherwise = y : i n s x ys ( i n s . 3)
--
i S o r t [3,9,2]
ins 3 (iSort D,2I ) by (iSort .2)
-- i n s 3 ( i n s 9 ( i S o r t [21))
ins 3 (ins 9 (ins 2 (iSort
i n s 3 ( i n s 9 ( i n s 2 [I 1)
[I 1)
by
by
by
( i S o r t .2)
( i s o r t .2)
( S o r t . 1)
--. i n s 3 ( i n s 9 C21) by ( i n s . 1 )
-.A i n s 3 ( 2 : i n s 9 [I ) by ( i n s . 3 )
- i n s 3 [2,9] by ( i n s . 1 )
1-. 2 : i n s 3 C91 by ( i n s . 3 )
-
--t 2 : [3,91
C2,3,91
by (ins.2)
Developing this function has shown the advantage of looking at examples while trying
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.
General recursions over lists 125
so that elemNum x x s 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?
7.7 Define a function
unique : : [ I n t l -> [Inti
so that unique xs returns the list of elements of x s which occur cxactly once.
For example, unique [ 4 , 2 , 1 , 3 , 2 , 3 ] is [ 4 , l ] . You might like to think of
two solutions to this problem: one using list comprehensions and the other not.
7.8 Give primitive recursive definitions of the prelude functions r e v e r s e a n d u n z i p .
7.9 Can you use the i S o r t function to find the minimum and niaximun~elements
of a list of numbers'! How would you find these elements without using i S o r t a ?
7.1 0 Design test data for the i n s function. Your data should address different possible
points of insertion, and also look at any exceptional cases.
7.1 1 By modifying the definition of the i n s function we can change the behaviour of
the sort, i S o r t . Redefine i n s in two difrerent ways so that
the list is sorted in descending order;
duplicates are removed from the list. For example.
iSort [2,1,4,1,23 = [1,2,4]
under this definition.
7.12 Design test data for the duplicate-removing version of i S o r t , explaining your
choices.
7.13 By modifying the definition of the ins and i S o r t f~unctions.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,731
is s n i d e r than ( 3 , 0 ) , and this is smaller than ( 3 , 2 ) .
In defining f ( x : xs) which values o f f y s would help me to work out the answer?
If each of the lists is non-empty, we form a pair from their heads, and then zip their
tails, giving
z i p (x:xs) ( y : y s ) = ( x , y ) : z i p x s y s ( z i p . 1)
but in all other cases - that is when at least one of the lists is empty -the result is empty:
zip - - = [I ( z i p . 2)
Note that we rely on the sequential nature of pattern matching here; we can give the
patterns for ( z i p . 2) explicitly if we wish, thus:
z i p (x:xs) (y:ys) = ( x , y ) : z i p x s ys
z i p (x:xs) [I = [I
z i p [I zs = [I
and in the second definition we see the lhree separate cases given in three separate
equations. Using the original definilion, an example calculation gives
z i p [1,5] [ ' c ' ,' d ' , ' e ' ]
-- ( 1 , ' ~ ' ) : z i p [51 [ ' d ' , ' e ' ] by ( z i p . 1 )
-
-- ( 1 , ' ~ ' ) : (5,'d') : z i p [I [ ' e ' l
( 1 , ' ~ ' ) : ( 5 , ' d ' ) : [I
by ( z i p . 1 )
by ( z i p . 2)
2. The function t a k e is used to take a given number of values from a list. For instance,
t a k e 5 "Hot Rats" = "Hot R"
t a k e 15 "Hot Rats" = "Hot Rats"
There are some special cases, when the I n t is zero, or the list is empty
take 0 - = [I ( t a k e . 1)
t a k e - [I = [I ( t a k e . 2)
What about the general case, when the list is non-empty and the I n t 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)
1 n>O = x : t a k e (n-1) x s ( t a k e . 3)
and in the other cases we give an error
take - - = e r r o r " P r e l u d e L i s t . t a k e : n e g a t i v e argument"
( t a k e . 4)
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
we can take off the head, 4, and then split the result [ 2 , 7 , 1 , 4 , 5 , 6 ] into two parts:
The first contains the elements no larger than 4, the second those exceeding 4. We sorl
these two, giving
It is striking to see how close this program is to our informal description of the algorithm.
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 i n the
recursive calls we apply qSort to two suhlists of xs, which rue necessarily smaller than
(x:xs).
In Chapter 19 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.
128 Definingfunctions over lists
Exercises
7.14 Using the definition of take as a guide, define the prelude functions drop and
splitAt.
7.15 What is the value of take (-3) [I 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.16 How would you define a function zip3 which z.ips together three lists? Try to
write a recursive definition and also one which u.ve.r zip instead; what are the
advantages and disadvantages of the two different definitions'?
7.1 7 How would you modify qSort to sort a list into descending order'? How would
you ensure that qSort removed duplicate elements?
7.18 One list is a suhlist of another if the elements of the firqt 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 suhsequence of another if it occurs as a sequence of elements ~ w x t
to euch 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 n subcequence
of another string.
In word processing systems it is customary for lines to be tilled and broken automatically.
t o enhance the appearance of the text. This book is no exception. Input of the form
The heat bloomed in December
as the carnival season
kicked into gear.
Nearly helpless with sun and glare, I avoided Rio's brilliant
sidewalks
and glittering beaches,
panting in dark corners
and waiting out the inverted southern summer.
would be transformed by filling to
The heat bloomed in December as the
carnival season kicked into gear.
Nearly helpless with sun and glare,
I avoided Rio's brilliant sidewalks
and glittering beaches, panting in
dark corners and waiting out the
inverted southern summer.
Example: text processing 129
To align the right-hand margin, the text is justified by adding extra inter-word spaces
on all lines but the last:
The heat bloomed in December as the
carnival season kicked into gear.
Nearly helpless with sun and glare,
I avoided Rio's brilliant sidewalks
and glittering beaches, panting in
dark corners and waiting out the
inverted southern summer.
An input file in Haskell can be treated as a string of characters, and so string-manipulating
operations play an important role here. Also, since strings are lists, this example will
exercise general list functions.
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, discarding
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.
In defining getword we will use the standard function elem, which tests whether an
object is an element of a list. For instance, elem 'aJ whitespace is False.
To guide the definition, consider two examples.
getword boo" should be " " as the first character is whitespace;
getword "cat dog" is "cat". We get this by putting 'c' on the front of "at",
which is getword "at dog".
The definition is therefore given by:
getword : : String -> String
getword [I = [I
getword (x: xs)
I elem x whitespace = [I
I otherwise = x : getword xs
130 Definingfunctions over lists
Consider an example
-
--i
--i
' c ' : ' a ' : ' t ' : getword
' c ' : ' a ' : ' t ' : [I
"cat"
dog"
dropword : : S t r i n g -> S t r i n g
dropword [I = [I
dropword ( x : x s )
I elem x whitespace = (x:xs)
I otherwise = dropword x s
It is easy to check that dropword " c a t dog" = " dog". We aim to use the functions
getword and dropword to split a string into its constituent words. Note that before
we take a word from the string " dog", we should remove the whitespace character(s)
from the front. The function dropspace will do this.
dropspace : : S t r i n g -> S t r i n g
dropspace [I = [I
dropspace ( x : x s )
I elem x whitespace = dropspace x s
I otherwise = (x:xs)
How is a string s t to be split into words? Assuming s t has no whitespace at the start.
t y p e Word = S t r i n g
s p l i t w o r d s : : S t r i n g -> [Word]
s p l i t w o r d s s t = s p l i t (dropspace s t )
s p l i t : : S t r i n g -> [Word]
s p l i t [I = [I
s p l i t st
= (getword s t ) : s p l i t (dropspace (dropword s t ) )
-
^i.j
^i.j
''dogn : s p l i t (dropspace " c a t " )
''dog1' : s p l i t l'cat'l
''dogn : (getword " c a t " )
: s p l i t (dropspace (dropword " c a t " ) )
--+ "dog" : " c a t " : s p l i t (dropspace [ I )
1.i "dog" : " c a t " : s p l i t [I
- lldogl1 : llcatll : [I
[ ''dog" , " c a t 1 ' I
getLine takes two parameters. The first is the length of the line to be formed, and the
second the list from which the words are taken. The definition uses l e n g t h to give the
length of a list. The definition will have three cases
In the case that no words are available, the line formed is empty.
If the first word available is w, then this goes on the line if there is room for it: its
length, l e n g t h w, has to be no greater than the length of the line, l e n .
The remainder of the line is built from the words that remain by taking a line of
length l e n - ( l e n g t h w+l).
If the first word does not fit, the line has to be empty.
getLine l e n [I = [I
getLine l e n (w:ws)
I l e n g t h w <= l e n = w : restOfLine
I otherwise = [I
where
newlen = l e n - ( l e n g t h w + 1)
restOfLine = getLine newlen w s
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
fill :: String -> [Line]
fill = splitLines . splitwords
To make the result into a single string we need to write a function
( Exercises >
7.19 Define the function dropLine specified in the text.
7.20 Give a detinition of the function
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.
'\n7.Define a similar function
which returns the same statistics for the text uftcr it has been filled.
7.25 Define a function
which tests whether a string is a palindrome -that is whether it is the same read
both backwards and forwards. An example is the string
Note that punctuation and white space are ignorcd in the test. and that no
distinction is made between capital and small letters. You might first like to
develop atest which simply tests whetherthe string is exactly the same backwards
and forwards, and only afterwards take account of punctuation and capital lelters.
7.26 [Harder] Design a function
so that
If the substring oldsub does not occur in st,the result should be st.
134 Defining functions over lists
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 tirst
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
bottorn-up approach to defining a library of functions.
( Chapter 8T7
Reasoning about
programs
8.1 Understanding definitions
8.2 Testing and proof
8.3 Definedness, termination and finiteness
8.4 A little logic
8.5 Induction
8.6 Further examples of proofs by induction
8.7 Generalizing the proof goal
-
l.t
length [XI
= length (x: [I) by defn of I x ]
= 1 + length [I by (length.2)
= l + O by (length.1)
= 1
The lesson of this discussion is that we can read a function definition in (at least) two
different ways.
We can take the dctinition as describing how to compute particular results, such as
length [2,3,I].
We can also take the definition as a general description of the behaviour of the
function in question.
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 tinding the maximum of three
integers. We can think of trying to prove that it does this. We need to look at various
cases of the ordering of the values. If we first look at the cases
138 Reasoning about programs
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,
the function also operates correctly. Finally, we start to look at the cases where precisely
two elements are equal. The function behaves correctly when
fact 2 f a c t (-2)
-
f a c t (-2)
(-2) * f a c t (-3)_
-
-- (-2)
...
* ((-3) * f a c t (-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
Finiteness
We have said nothing so far about the order in which expressions are evaluated in
Haskell. In fact, Haskell evaluation is lazy, s o 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
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
8.1 Given the definition of f a c t above, what are the results of evaluating the
following expressions?
140 Reasoning about programs
Discuss the reasons why you think that you obtained these answers.
A little logic
In order lo appreciate how to reason about functional programs we nccd not have a
background in Sormal logic. Nevertheless, it is worth discussing two aspects of logic
before wc proceed with our proofs.
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 a.s.vlme that pctrol costs 27
pcncc per litre, then we can prove that four Iitres will cost 1.08.
What does this tell us? I t does t ~ o tell
t us outright how much four litres will cost: it
only tells us the cost ifthe a.s.sirt?~ptiori
i s i d i d . To be sure that the cost will be 51.08,
we need to supply some evidence that the assumption isjustified: this might be another
proof - perhaps based on petrol costing f 1.20 per gallon - o r direct evidence.
We can write what we have proved as a formula,
square x = x*x
lnduction 141
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 thus
Vx (square x = x*x)
where we read the universal quantifier, 'Vx', as saying 'for all x. . . '
We now turn to induction, the main technique we use for proving properties of progranis.
(85) lnduction
In Chapter 7 we w w that a general mcthod for defining list\ wa\ primitive recur\lon,
as exenlplitied by
sum : : [ I n t ] -> I n t
sum [I = 0 . , (sum. 1)
sum (x:xs) = x + sum x s (sum. 2)
Here we give a value outright at [I. and detine the value of sum ( x : x s ) using the
value sum xs. Structural induction is a proof principle which slates:
( Definition
Principle of structural induction for lists
In order to prove that a logical property P (xs) holds for all finite lists x s we have
to do two things.
Base case. Prove P ( [I outright.
Induction step. Prove P (x :xs) on the assumption that P ( x s ) holds.
In other words P ( x s ) j P (x :xs) has to be proved.
The P (xs) here is called the induction hypothesis since it is assumed in proving
P(x:xs).
I t is interesting to see that this is just like primilive recursion. cxcept that instead of
building thc values of a function, we are building up the parts of a proof. In both cases
we deal with [I as a basis. and then build the general thing by showing how to go tiom
x s to (x:xs). In a function definition we define fun ( x : x s ) using fun xs; in the
proof of P (x :x s ) we are allowed to use P (xs) .
Justification
Just as we argucd that recursion was not circular. s o we can see proof by induction
building up the proof for all finite lists in stages. Suppose that we are given proofs of
P([1) andP(xs) j P(x:xs) f o r a l l x a n d x s a n d w e w a n t t o s h o w t h a t P ( [ 1 , 2 , 3 1 ) .
The list [1,2,31is built up from [I using cons thus,
142 Reasoning about programs
and we can construct the proof of P ( [ I , 2,311 in a way which mirrors this step-by-step
construction.
P ( [I ) holds;
Recall our discussion of '+' above; if we know that both P ( [I ) jP ( [31) and
P ( [I ) hold, then we can infer that P ( [31) holds.
P ( [3] ) + p ( [2,3] ) holds, and so for similar reasons we get P ( [2,31).
Finally, because P ( [2,3] ) j P ( [ I , 2,3] ) holds, we see that P ( [ 1,2,31) holds.
This explanation is for a particular finite list, but will work for any finite list: if the list
has nelements, then we will have n + l 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.
I
A first example
We have mentioned the definition of sum;recall also the function to double all elements
of a list
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:
.
= 2*x + 2 * sum xs
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 therefore the
proof itself.
We use the box,. , to signify the end of a proof.
144 Reasoning about programs
In this section we present two more examples of proof by structural induction over tinitc
lists.
( Examples )
1. length and ++
We begin by looking at the example (length.4) introduced at the start of the chapter.
length (xs ++ YS) = length xs + length ys (length.4)
where we have choqen new names for the variables so as not to conflict with the variable\
In the goal.
There is some question about how to proceed with the proof, since (length.4)
involvec two variables, xs and ys. We can be guided by the definitions, where we see
that the definition of ++ is made by recursion over thejfiest variable. We therefore make
Further examples of proofs by induction 145
the goal a proof of ( l e n g t h . 4) for all finite x s 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 ( l e n g t h . 3) stood for an arbitrary list element.
Statement We can now write down the two goals of the induction proof. The base
case requires that we prove
l e n g t h ( ( x : x s ) ++ y s ) = l e n g t h (x:xs) + l e n g t h y s (ind)
l e n g t h (xs ++ y s ) = l e n g t h x s + l e n g t h y s (~YP)
Base We look separately at the two sides of (base), left-hand side tirst,
l e n g t h ([I ++ ys)
= l e n g t h ys
-
length [I + l e n g t h ys
= 0 + length ys
= l e n g t h ys
l e n g t h ( ( x : x s ) ++ ys)
= l e n g t h (x: (xs ++ y s ) ) by (++. 2 )
= 1 + l e n g t h ( x s ++ ys) by ( l e n g t h . 2 )
We cannot simplify this further with the defining equations, but we can use (hyp) t o
give us
= 1 + length xs + length ys by ( ~ Y P )
l e n g t h ( x : x s ) + l e n g t h ys
= 1 + l e n g t h x s + l e n g t h ys by ( l e n g t h . 2)
and this shows that ( i n d ) follows from (hyp), completing the second half ofthe proof
and thus the proof itself.
146 Reasoning about programs
2.reverse and ++
What happens when we reverse the join of two lists, xs++ys?
where we define
reverse [I = [I (reverse.I)
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.
reverse ys ++ reverse [I
= reverse ys ++ [I by (reverse.1)
but we can prove the two equal only if wc can show that appending an empty list to the
end of a list is an identity operation, that is
Induction Again, we look at the two sides o f the equation, left-hand side first.
Now, these two are ulmost equal, except that the joins are bracketed differently. W e
need another general property o f ++, namely that it is associative:
( Exercises 1 I
unzip [I = ( CI , CI
unzip ( (x ,y) :ps)
= (x:xs,y:ys)
where
(xs,ys) = unzip ps
take n xs ++ drop n xs = xs
shunt 11 ys = ys (shunt.1)
shunt (x:xs) ys = shunt xs (x:ys) (shunt.2 )
Starting with an empty second argument, we have
Generalizing the proof goal 149
by proving it for an arbitrary zs.The left-hand side simplifies to the right-hand side in
one step.
= shunt (x:zs) xs by ( ~ Y P )
= shunt zs (x:xs) by (shunt.2)
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 seeins paradoxical: we are making it harder for
ourselves, apparently. We are in one way, but at the same time we make the induction
hypothesis stronger, so that we have more resources to use when proving the induction
step.
( Exercises )
8.9 Prove for all finite lists xs and ys that
we can define
fac2 n = facAux n 1
fac n = fac2 n
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 definitions.
We gave a collection of hints about how we can build proofs for functional programs.
and illustrated these by giving a number of results for common prelude functions such
as sum, ++ and length,as well as exercises involving others.
Generalization: patterns
of computation
9.1 Patterns of computation over lists
9.2 Higher-order functions: functions as arguments
9.3 Folding and primitive recursion
9.4 Generalizing: splitting up lists
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 functions 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 corn-
putes 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 computa-
tion; 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
Patterns of computation over lists 153
taking the se?ond element o f each pair in a list of pairs, as we do i n the library
database;
i n the supermarket billing example, converting every item i n a list of bar codes to the
corresponding (Name ,Price) pair;
formatting each (Name,Price) pair in a list.
In a sinlilar 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 Rooleans 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 havc w m e property. A first example is getword,
getword "cat dog" = "cat"
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 ++.
Higher-order functions: functions as arguments 155
iSort [I = [I
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
splitLines [I = [I
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 recursive
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 i n sensible cases where no word in the list ws is longer than the
line length lineLen.
doubleAll [I = [I
doubleAll (x:xs) = 2*x : doubleAll xs
In both cases. we can see that the specitic operation of multiplying by two is applied to
an element of the list in the expression '2*x'.
Suppose that we want to modify every element of a list by another operation - for
instance. the function ord that transl'orms a Char into an Int - w e could modify one of
the definitions above by replacing the '2*x' by 'ord x' to give a different definition.
Taking this approach would mean that we would write a whole lot of definitions
which differ only in the fi~nctionused to make the transformation. Instead of doing
this, we can write a single definition in which the function becomes a parameter of the
definition. Our general definition will be
map f [I = [I (map.1)
map f (x:xs) = f x : map f xs (map.2)
The function to double all the elements of a list can now be given by applying map to
two things: the transformation - double - and he list in question.
where double x = 2*x. In a similar way, the function to convert all the characters
into their codes will he
In the Picture case study to flip a picture in a vertical mirror we can write
What is the type of map? It takes two arguments t h e first is a function, and the second
is a list -and it returns a list.
Higher-order functions: functions as arguments 157
The figure shows how the types of the functions and lists are related, giving map thc
tY Pe
where recall that a and b are type variables, standing for arbitrary types. Instances o f
the type of map include
earlicr. How is the property of 'being a digit' to be modclled? Wc have already seen
that the prelude contains a function
and we find out whether a particular character like ' d J is a digit o r not by applying the
function to the character to give a Boolcan result, that is True or F a l s e .
This is the way that we can model a property over any type t . The property is given
by a function of type I
t -> Bool
and an cleincnt x has the property precisely when f x has the valuc True. We have
already seen the example of isDigit; other examples include
158 Generalization: patterns of computation
i s s o r t e d : : [ I n t ] -> Bool
i s s o r t e d x s = ( x s == S o r t x s )
where we usually adopt the convention that the names of properties begin with ' i s ' .
which combines two lists into a list of pairs, where we pair corresponding elements in
the two lists. For instance,
In the first case we see that if both lists are non-empty we apply the function f to their
heads to give the first element of the result, and zip their tails with f in a similar way.
In the second case - when at least one of the inputs is [I - the result is [I. just as it
was in the definition of zip.
Returning to the Picture case study, we can then detine
What is the type of zipwith? The function takes three arguments. The second and
third are lists of arbitrary type, [a] and [b] respectively. The result is also a list of
arbitrary type, [cl. Now, the first argument is applied to elements of the input lists
to give an element of the output list, so it must have type a -> b -> c. Putting this
together, we have
In the exercises we look further at the examples defined here, as well as introducing
other higher-order functions.
9.1 Write three line-by-line calculations of doubleAll [2,1,7] using the three
different definitions of doubleAll by means of a list comprehension, primitive
recursion and map.
9.2 How would you define the length function using map and sum'?
9.3 Given the funytion
where
Can you conclude anything in general about properties of map f (map g xs)
where f and g are arbitrary functions'?
9.5 What is the effect of
f i l t e r greaterOne ( f i l t e r lessTen n s )
f i l t e r p ( f i l t e r q xs)
i t e r n f x = f (f if ... (f X I . . . ) )
Folding and primitive recursion 161
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))
a
Folding and primitive recursion
f o l d r f s [I = s (f o l d r . 1 )
f o l d r f s (x:xs) = f x ( f o l d r f s xs) ( f o l d r .2)
The 'r' in the definition is for 'fold, bracketing to the right'. Using this slightly more
general function, whose type we predict is
Returning to the start of the section, wc can now see why f o l d r l is so called: it is fold
function, designed to take a list with at least one element. We can also define f o l d r l
from f o l d r , thus
f o l d r l f (x:xs) = f o l d r f x x s (foldrl.0)
( Exercises )
9.11 How would you define the sum of the squares of the natural numbers 1 t o n using
map and f o l d r ?
9.12 Define a function to give the sum of squares of the positive integers in a list of
integers.
9.13 For the purposes of this exercise you should use f o l d r to give definitions of the
prelude functions unzip, l a s t and i n i t , where examples of the latter two are
given by
[I (map s i n g x s )
mystery xs = f o l d r (++I
9.15 The function f ormatLines is intended to format a list of lines using the function
f o r m a t l i n e : : Line -> S t r i n g
a -> S t r i n g
to forinat each item of the list which is passed as the second parameter. Show
how f ormatLines can be defined using f ormatList and f ormatline.
9.18 How can you simplify some of your earlier definitions in the light of the higher-
order functions you have seen here? You could revisit the 'supermarket billing'
exercises and try doing those questions again using the functions you have now
seen.
Generalizing: splitting up lists 165
where t a k e n x s and drop n x s are intended to take or drop n elenients 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.
It is a general principle of functional programming that programs can often be
rewritten to use more gencral polymorphic and/or higher-order functions, and we
illustrate that here.
The function getword was originally defined thus:
getword : : S t r i n g -> S t r i n g
getword [I = [I
getword (x:xs)
I elem x whitespace = [I
I otherwise = x : getword x s
What forces this to work over strings i4 the test in (getword. 21, where x is checked
for membership of whitespace. We can generalize the function to have the test - or
property - as a parameter.
How is this to be done? Recall that a property over the type a is represented by a
function of type ( a -> Bool). Making this test a parameter we have
in which the test elem x whitespace has been replaced by the test p x. the arbitrary
property p applied to x. We can of course recover getword from this definition:
getword x s
= g e t u n t i l p xs
where
p x = elem x whitespace
166 Generalization: patterns of computation
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,
9.19 Give the type and definition of the generalization dropuntil of the function
dropword.
9.20 How would you define the function dropspace using dropunt il'? How would
you define takewhile using getuntil?
9.21 How would you split a string into lines using getuntil and dropuntil'?
9.22 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.
9.23 Can you give generalizations to polymorphic higher-order functions of the text
processing functions getline, dropLine and splitlines?
( 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 f oldr. 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.
We concluded with 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 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 functions - this
is the topic of the next chapter.
( Chapter 10 )
Functions as values
10.1 Function-level definitions
10.2 Function composition
10.3 Functions as values and results
10.4 Partial application
10.5 Revisiting the Picture example
10.6 Further examples
10.7 Currying and uncurrying
10.8 Example: creating an index
10.9 Verification and general functions
Function-level definitions
One of the reasons that functional programming is called 'functional' is that in such
a language we can deal with functions as data, and so treat them much as we might
handle integers or lists. Because of this, we will see in this chapter that we can often
give a function-level definition of a function. What do we mean by this? Rather than
explaining how a function operates on one or more parameters, as in the definition
r o t a t e : : P i c t u r e -> P i c t u r e
rotate pic = flipV (flipH pic) ( r o t a t e . 1)
r o t a t e = flipV . flipH ( r o t a t e . 2)
flipH : : P i c t u r e -> P i c t u r e
flipH = reverse
We note that this definition has exactly the same effect as saying
since if we were to use (f l i p H . I ) applied to the picture horse, say, the first step of
the evaluation would be the step
flipH horse
2, reverse horse
Not all pair\ of functions can be composed. The output of g, g x, beconles the input of
f , yo that the output type of g mu\t equal the input type o f f . In the example of r o t a t e
from Section 10.1 we see that the output type o f f lipH and the input type o f f lipV are
both P i c t u r e .
In general, the constraint on which functions can be compo\ed i \ expres5ed by giving
'.' the type
which shows that. if we call the tirst input f and the second g,
The input o f f and the output of g are of the same type: b.
The result f . g has the same input type, a, as g and the same output type, c, as f .
Composition is associative, that is f . ( g . h) is equal to (f . g ) . h for all f .
g and h. We can therefore write f . g . h unambiguously to mean 'doh, then g, then
f ?.I
Forward composition
The order in f . g is significant, and can be confusing; (f . g ) means 'tirst apply g
and then apply f to the result', and so we have to read a composition from right to left
in order to appreciate its effect.
' For tcchnical reasons. the ' . ' is trealed as right associalivc in the Haskell standard prelude
170 Functions as values
The reason we write (f . g) for 'g then f ' is that we write arguments to the right of
functions. The argument is therefore closer t o g than to f , and the order of the functions
in (f . g) x is the same as in the nested application, f ( g x ) .
It is simple in Haskell todefine an operator for composition which takes its arguments
in the opposite order to ' . '. This we do thus:
g >.> f = f , g (fcomp. 1)
( g >.> f) x = (f . g) x = f (g X) (f comp. 2 )
showing that. as it were, the order of the f and g is swapped before the functions are
applied. The r o t a t e example can then be written
r o t a t e = f l i p H >.> f l i p V
which we can read as f lipH then f lipV, with the functions being applied from left to
right.
The notation '>. >' contains a ' . ' to show that it is a form of composition, with the
arrows showing the direction i n which information is flowing. We will tend to use '> .>'
in situation\ where a number of functions are composed, and it is therefore tiresome to
read some lines down the page in order to work out the effect of a function definition.
Pitfalls of composition
There are two pitfalls associated with coniposition which we need to be aware of:
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->b, whereas it actually has type Bool.
In applying a composition we therefore need to be sure that it is parenthesized, as
follows:
10.1 Redefine the function printBill from the supermarket billing exercise in
Section 6.4 so that composition is used. Repeat the exercise using forward
composition. > . >.
10.2 If id is the polymorphic identity function, defined by i d x = x. explain the
behaviour of the expressions
(id. f) (f . id) id f
I f 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 i d is properly typed?
10.3 Define a function composeList which composes a list of functions into a single
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'?
-
?--
iter n f
I n>O = f . i t e r (n-1) f ( i t e r . 1)
I otherwise = id (iter.2)
This is a standard primitive recursion over the integer argument; in the positive case
we take the composition o f f with itselfn-1 times and compose once more with f . In
the zero case we apply f n o times, so the result is a function which does nothing to
its argument, namely i d . We can give a constructive definition using the standard list
functions.
iter n f = foldr ( . ) i d (replicate n f ) (iter.3)
In this definition we create the list of n copies o f f
[ f , f , . . . ,f l
which is then composed by folding in the composition operator to give
f . f . ... . f
As an example. we can dctine 2" as i t e r n double 1, if double doubles its argunirnt.
\x Y -> g (f x) (f y)
The result is a function named addN, and addN is itself defined by an equation in thc
where clause. This method is rather indirect - w e say we shall return the function
named addN, and then detine that Function.
Lambda notation
Instcad of naming and defining a function that w e want to refer to. we can instead write
it down directly. In the case of defining addNum w e can define the result as
Before the arrow are the arguments, in this case the single argument m.
After the arrow comes the resull. here n+m.
That the expression is a function is signalled by its beginning with ' \ ' which is the
closest ASCII character to the Greek lambda; h, which is used in a mathematical theory
of functions, called the lambda calculus, for exactly this purpose. The deti~iitionof
addNum now becomes
We shall see another way of defining addNum in the next section of this chapter.
Another example which uses the lambda notation is given by the 'plumbing' illus-
trated in Figure 10.2. The object shown is n function, whose arguments are x and y.
The result of the function is
174 Functions as values
so the overall effect is to give a function which applies f to each of its (two) arguments
before applying g to the results. The definition states this quite straightforwardly:
comp2 sq add 3 4
f x y z = result
\x y z -> result
iter 3 double 1
(comp2 succ ( * I ) 3 4
comp2 sq add 3 4
10.6 Given a function f of type a -> b -> c, write down a lambda expression that
describes the function of type b -> a -> c which behaves like f but which
takes its arguments in the other order. Pictorially,
Partial application 175
10.7 Using the last exercise, or otherwise, give a definition of the function
which reverses the order in which its function argument takes its arguments.
10.8 Using a lambda expression, the Boolean function n o t and the built-in function
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 V .
which takes a function f as argument, and returns (an approximation to) its
derivative f ' as result.
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.
m u l t i p l y : : I n t -> I n t -> I n t
m u l t i p l y x y = x*y
We can view the function as a box, with two input arrows and an output arrow.
176 Functions as values
i multiply
If we apply the function to two arguments, the result is a number; so that, for instance.
multiply 2 3 equals 6.
multiply
3
=Ll- 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 exalnple o f 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.
To illustrate, we return again to our example in which every element of a list is to he
doubled. The function can be defined thus:
doubleAl1 : : [Int] -> [Inti
doubleAl1 = map (multiply 2)
but, as was argued in Section 10.1, there are advantages to this form of definition.
In Section 10.3 we saw the example of addNum,
Partial application 177
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
since when addNum is applied to one argument n it returns the function adding n.
The idea of partial application is important. We have already seen that many functions
can be defined as specializations of general operations like map, filter and so on.
These specializations arise by passing a function to the general operation -this function
is often given by a partial application, as in the examples from the pictures case study
first seen in Chapter I :
We return to look at the Picture case study in greater detail in Section 10.5.
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. Consider the function
elem ch whitespace
where whitespace is the string " \t\nM. We would like to write the function to test
this by partially applying elem to whitespace, but cannot. We could define a variant
of elem which takes its arguments in the other order, as in
member xs x = elem x xs
member whitespace
In a similar vein, to filter all non-whitespace characters from a string, we could write
either of the partial applications
/ Definition )
Rule of cancellation
If the type of a function f is
For example, using this rule we can see that we get the following types
multiply 2 :: I n t -> I n t
multiply 2 3 :: Int
doubleAll :: [Int] -> [ I n t l
doubleAll [2,3] :: [Intl
The function space symbol '->' is right associative, so that a -> b -> c means
a -> ( b -> c)
and not
( a -> b) -> c
The arrow is not associative. If
f : : I n t -> I n t -> I n t
g : : ( I n t -> I n t ) -> I n t
as illustrated
Partial application 179
and so it can therefore he applied to an integer. Doing this gives (for example)
m u l t i p l y 2 : : I n t -> I n t
This can itself be applied to give
( m u l t i p l y 2) 5 : : I n t
which, since function application is left associative, can be written
multiply 2 5 :: I n t
Our explanations earlier in the book are consistent with this full explanation of the
system. We hid the fact that
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.
Examples of partial applications will be seen throughout the material to come, and
can be used to simplify and clarify many of the preceding examples. Three simple
examples are the text processing functions
180 Functions as values
where
member xs x = elem x xs
We look at further examples in the next section, after examining partially applied
operators.
Operator sections
The operators of the language can be partially applied, giving what are known as
operator sections. Examples include
(+2) The function which adds two to its argument.
(2+) The function which adds two to its argument.
(>2) The function which returns whether a number is greater
than two.
(3: The function which puts the number 3 on the front of
a list.
(++"\ntl) The function which puts anewline at theend o f a string.
( "\nBl++) The function which puts a newline at the beginning of
a string.
The general rule here is that a section of the operator op will put its argument to the
side which completes the application. That is,
(op x) y = y op x
(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,
is the function which adds one to each member of a list, and then removes those eleinents
which are not positive.
10.1 2 Use partial applications to define the functions comp2 and total given in Section
10.3 and its exercises.
10.13 Find operator sections seci and sec2 so that
map secl . filter sec2
has the same effect as
filter (>O) . map (+I)
Revisiting the Picture example 181
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
above : : Picture -> Picture -> Picture
above = (++)
while placing the pictures side-by-side requires corresponding lines to be joined together
with ++, using the function zipwith first introduced in Section 9.2.
sideBySide : : Picture -> Picture -> Picture
sideBySide = zipwith (++)
Among the other functions mentioned were
invertcolour : : Picture -> Picture
superimpose : : Picture -> Picture -> Picture
printpicture : : Picture -> I0 ()
and we give their definitions now. To invert the colour in a picture, we need to invert
the colour in every line, so
invertcolour = map .. .
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.1. This gives the definition
invertcolour : : Picture -> Picture
invertcolour = map (map invertchar)
which we can read as saying
182 Functions as values
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
since the e f i c t of thir ir tirst to add a newline character to every line - the role of map
(++"\nl')- and then to join this list of strings into a single string - the effect of the
concat. We therefore define the printing function thus:
( Exercises 1
In these exercises we suggest further operations over pictures.
Revisiting the Picture example 183
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,
It is evident from this that positions within lines and lines themselves are counted
from zero, with line zero being the top line.
10.17 Define a function
discuss how you would define functions over Rep to rotate, reflect and superim-
pose pictures under this alternative representation. Discuss the advantages and
disadvantages of this representation in comparison with the original representa-
tion given by the Picture type.
10.19 In the light of the discussion in the last four chapters, redo the exercises of
Section 6.2, which deal with positioned pictures.
184 Functions as values
as required.
Finally, the function getword can itself be given a direct definition, by partial
application thus
getword = getuntil ('elem' whitespace)
This definition reads like an informal explanation - to get a word, get characters until
a whitespace character is found.
Currying and uncurrying 185
curry g
This function expects its arguments as a pair, but its curried version, c u r r y g, will take
them separately we therefore have to form them into a pair before applying g to them:
-
uncurry f
In Lhct the t i n t person to describe the idea was Schiintinkel, but 'SchBntinkeling7 does not sound somappy!
186 Functions as values
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 : : ( a -> b -> c ) -> ( ( a , b ) -> c )
uncurry f (x,y) = f x y
uncurry m u l t i p l y will be exactly the same function as multiplyUC. The functions
c u r r y and uncurry are inverse to each other.
Partial application of functions is done on the arguments from left to right, so a
function cannot directly be applied to its second argument only. This effect can be
achieved indirectly by first transforming the order in which the function takes its
arguments and then partially applying it.
f l i p : : ( a -> b -> c ) -> ( b -> a -> c)
flipf x y = f y x
f l i p map will takes as its first argument the list and as its second the function to be
mapped; it can be applied to its first argument, having the effect of applying map to its
second only.
Another way of forming the partial application ( ' elem' whitespace) is to use the
f l i p function. We have
f l i p elem : : [Char] -> Char -> Boo1
(among other types) and so we can form the partial application thus:
f l i p elem whitespace
We now turn to a more substantial example in which we use the ideas of composition,
partial application and operator sections in a variety of ways.
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
Example: creating an index 187
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 o f
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
moditications to the data with which we begin. This data-directed design is common
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)]
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 [ ( [Intl ,Word)].
Amalgamate entries for the same word into a list of numbers, giving a list of type
C ( [Intl ,Word)].
Shorten the list by ren~ovingall entries for words of less than four letters, giving a
list of type [ ( [Int] ,Word)I .
188 Functions as values
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 >.> -- [(~nt,Line)1 -> [(Int ,Word)]
sortLs >.> -- [(Int ,Word)] -> [(Int ,Word)]
makeLists > .> -- [ (1nt ,Word>1 -> 1( [Intl ,Word)1
amalgamate > .> -- [ ( [Intl ,Word)1 -> C ( [Intl ,Word)1
shorten -- [ ( [Intl ,Word)] -> [ ( [Inti ,Word)]
Once thc type of each of the functions is given, developn~ento f 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 bc given. in
turn.
Now the lines have to he split into words, and line numbers attached. We lirst consider
the problen~for a single line.
numWords : : ( Int , Line ) -> [ ( Int , Word ) ]
Splitting into words can be done by the function splitwords of Chapter 7, tiioditied -
slightly. When we defined splitwords we preserved any punctuation characters. as
these werc to appear in Lhe output of the text processor. In contrast here we will modify
the definition of whitespace to include punctuation, and so remove the punctuation from
the resulting words. We define
Example: creating an index 189
whitespace : : S t r i n g
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 n list,
the list of words making up the line. This is a job for map,
numWords (number , l i n e )
= map (\word -> (number,word)) ( s p l i t w o r d s l i n e )
or a list comprehension
numWords (number , l i n e )
= [ (number , word) I word <- s p l i t w o r d s l i n e 1
To apply this to the whole text, the function nunwords has to be applied to every line.
This is again done by map, and the individual results joined together or concatenated.
We make a direct definition of the function. by composing its two parts. First we map
the function nunwords, then we concatenate the results, using concat.
What has been achieved so far'! The text has been transformed into a l i \ t of line-
numbcrlword pail-s, from which an index is to be built. For instance, the text
will be convcrted to
The list n ~ u s tnext be sorted by word order, and lists of lines o n which a word appears
be built. The ordering relation on pairs of numbers and words is given by
The words are compared for dictionary order. For pair\ containing the wme worcls,
ordering is by line number.
Sorting a list is most eacily done by a version of the quicksort algorithm. The li\t i \
split into parts smaller than and larger than a given clement; each of thew halve\ can
be sorted xparately, and then joined together to form the result.
s o r t L s [I = [I
sortLs (p:ps) = s o r t L s s m a l l e r ++ [p] ++ s o r t L s l a r g e r
190 Functions as values
The lists s m a l l e r and l a r g e r 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 s m a l l e r or l a r g e r .
How are the two lists defined'? They are given by selecting those elements of ps
with given properties: a job for f i l t e r , or a list comprehension. Going back to the
definition of s o r t l s ,
s o r t L s ( p :p s )
= s o r t L s s m a l l e r ++ [p] ++ s o r t L s l a r g e r
where
s m a l l e r = [ q I q<-ps , o r d e r p a i r q p I
l a r g e r = [ q I q<-ps , o r d e r p a i r p q 1
The entries for the sarne word need to be accumulated together. First each entry is
converted to having a list of line numbers associated with it, thus
makeLists : : [ ( I n t ,word) 1 -> [ ( [ I n t ] ,Word) 1
makeLists
= map mklis
where
mklis ( n , s t ) = ( [nl , s t )
After this, the lists associated with the same words are amalgamated.
amalgamate [I = [I
amalgamate [p] = [pl
amalgamate ( ( 1 1 ,wl) : (12,w2) : r e s t )
1 w l /= w2 = (l1,wI) : amalgamate ((12,w2) : r e s t ) (amalg.1)
I otherwise = amalgamate ( ( 1 1 + + 1 2 , w I ) : r e s t ) (amalg .2)
The frst two equations are simple, with the third doing the work.
If we have two adjacent entries with different words, case (amalg. I ) , 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 ) , hey 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.
Example: creating an index 191
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 (n1,wd) = length wd > 3
Again, the filter function proves useful. The index function can now be defined in
full:
makeIndex : : Doc -> [ ( [Int] ,Word) 1
makeIndex
= lines >.> numLines >.> allNumWords >.> sortLs >.>
makeLists > . > amalgamate >.> shorten
As was said at the beginning of this section, function composition provides a powerful
method for structuring dcsigns: programs are written as a pipeline of operations,
passing the appropriate data structures bctween them.
It is easy to see how designs like these can be modified. To take one example, thc
indexing program above filters out short words only as its final operation. Therc 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.
10.20 Detine the function lines using the functions getuntil and dropuntil from
Chapter 9, or the built-in functions takewhile and dropwhile. You should be
careful that your functions d o not give an empty word when there are empty lines
in the Doc; this might happen for the examples "cat\n\ndogW and "f ish\nV.
10.21 How would you use lambda expressions to replace the local detinitions in
makeLists and shorten'? How would you define these functions using list
comprehensions?
10.22 I n the index for this book, instead of printing an entry like
cathedral 3, 5, 6, 7, 9, 10
192 Functions as values
How would you redesign your program to do this'? Hint: tirst 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.
10.23 How would you re-define sortLs so that duplicate copies of an item are 17ot
removed? For the index, this means that if a word occurs twicc o n line 123 say.
then 123 occurs twice in the index entry for that word.
10.24 How could the functions getuntil and dropuntil be used in the delinition of
amalgamate?
10.25 Explain how the function sizer defined locally in shorten can be dctined 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.
10.26 How is the following definition of the last conditional equation for amalgamate
incorrect? Give an example calculation to justify your answer.
amalgamate ((ll,wl):(12,~2):rest)
I wl /= w2 = (11,wl) : amalgamate ((12,w2):rest)
I otherwise = (11++12,wI) : amalgamate rest
which gives a neatly laid-out printable version of an index, as shown at the start
of the section. You might find it useful to define a function
10.30 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?
10.31 The function s o r t L s is limited to sorting lists of type [ ( I n t ,Word)] because
it calls the o r d e r p a i r function. Redefine the function so that it takes the
comparison function as a pammeter. What is its type after this redefinition?
10.32 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.
Function-level verification
We claimed in Section 10.3 that the function i t e r is a generalization of twice, since
iter 2 f
= f . iter 1 f by ( i t e r . 1 )
= f . ( f . iterOf) by ( i t e r . 1)
= f . (f , i d ) by ( i t e r - 2 )
= f . f by (compId)
= twice f by ( t w i c e . 1)
In proving this we have used the equality between two functions
How is this proved? We examine how each side behaves on an arbitrary argument x
(f . id) x
= f ( i d 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.
Definition
Principle of extensionality:
Two functions f and g are equal if they have the same value at every argument.
194 Functions as values
This is called extensionality in contrast to the ideaof intensionality in which wc hay two
functions are the same only if they have the same definitions - we no longer think of them
as black boxes; we are allowed to look inside them to see how the mechanisms work.
as it were. If we are interested in the results of our programs, all that matters are the
values given by functions, not how they are arrived at. We therefore use extensionality
when we are reasoning about function behaviour in Haskell. If we are interested in
efficiency or other performance aspects of programs, then the way in which a result is
found will be signiticant, however. This is discursed further in Chapter 19.
( Exercises 1
10.33 Using the principle of extensionality, show that fi~nctioncomposition is associa-
tive: that is, for all f , g and h,
flip . flip = id
Hint: to show this, you might want to prove that for any f ,
flip (flip f ) = f
Prove that the functions c u r r y and uncurry of Section 10.7 are inverses. Can
you think of other pairs of inverse functions?
i t e r n id = id
Show that the functions abs and signum are idempotent. Can you think of any
other idempotent functions?
Verification and general functions 195
Higher-level proofs
Our verification thus far has concentrated on first-order, monomorphic functions. Just
as map, f i l t e r and f o l d 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
doubleAl1 (xs++ys) = doubleAll x s ++ doubleAll y s
holds for all finite lists x s and ys. When doubleAll is defined as map ( * 2 ) it becomes
clear that we have an example of a general result,
map f (xs++ys) = map f x s ++ map f ys (map++)
which is valid for any function f . We also claimed in an earlier exercise that
sum (xs++ys) = sum x s + sum ys (sum. 3)
for all finite lists xs, ys. The function sum is given by folding in (+I,
sum = f o l d r (+) 0
and we have, generally, i f f is associative, and s t is an identity for f , that is,
x ' f ' (y ' f ' z ) = ( X ' f ' y) ' f ' z
X ' f ' st = X = st ' f ' X
for all x, y, z then the equation
f o l d r f st (xs++ys) = f ( f o l d r f st x s ) ( f o l d r f st y s ) (foldr.3)
holds for all finite x s and ys. Obviously (+) is associative and has 0 as an identity.
and so (sum. 3 ) is a special case of ( f o l d . 3 ) . Now we give three proofs of examples
in the same vein.
map (f . g) [I = [I by (map.1)
(map f . map g) [I
= map f (map g [I by (comp. 1)
= map f [I by (map.1)
= [I by (map.1)
Assuming that
map (f . g) xs = (map f . map g ) xs (~YP)
is true, it is now necessary to prove that
map (f . g ) (x:xs) = (map f . map g ) ( x : x s ) (ind)
Again, it is enough to analyse each side of the equation.
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)
(map f . map g ) ( x : x s )
= map f (map g ( x : x s ) ) by (comp . I)
= map f (g x : map g x s ) by (map. 2)
= f (g X) : map f (map g x s ) by (map. 2)
= f (g X) : (map f . map g ) x s 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. I
Each Haskell list type, besides containing finite lists, also contains infinite and parlial
lists. In Chapter 17 these will be explained and it will be shown that (map. 3) is true
for ~111lists XS,and therefore that the functional equation
map and f i l t e r
The proof above showed how properties of functional programs could be proved from
the definitions of the functions in a straightforward way. The properties can state how
the program behaves - that a sorting function returns an ordered list, for instance -or
can relate one program to another. This latter idea underlies program transformation
for functional languages. This section introduces an example called filter promotion
which is one of the most useful of the basic functional transformations.
Verification and general functions 197
and it is clear that here the transformed version is more efficient, since the test (O<=)
is no more costly than (O<). The proof that
(filter p . map f ) xs = (map f . f i l t e r (p . f ) ) xs
for finite lists x s is by structural induction. First we reiterate the definitions of map,
f i l t e r and composition.
map f [I = [I (map. 1)
map f (x:xs) = f x : map f x s (map. 2 )
f i l t e r p [I = [I ( f i l t e r .I)
f i l t e r p (x:xs)
I PX = x : f i l t e r p xs ( f i l t e r .2)
I otherwise = f i l t e r p xs ( f i l t e r .3)
(f . g) x = f (g x) (comp . I )
The base case consists of a proof of
(filter p . map f 11 = (map f f i l t e r (p . f 1) [I (base)
Thih is true since
( f i l t e r p . map f ) [I
= f i l t e r p (map f [I) by (comp. I)
= f i l t e r p [] by (map. 1)
= [I by ( f i l t e r . I )
and
(map f . f i l t e r (p . f ) ) [I
= map f ( f i l t e r (p . f ) [I) by (comp. 1)
= map f [I by ( f i l t e r . 1)
= [I by (map.1)
In the induction step, a proof of
198 Functions as values
reverse [I = [I
reverse (z:zs) = reverse zs ++ [zl
Base Looking at the two sides of the base case in turn, we have
map f (reverse [I )
= map f [I by (reverse.I)
= [I by (map.1)
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.
(we leave this proof as an exercise for the reader) and using (map++) we can continue
to simplify the left-hand side
= map f ( r e v e r s e x s ) ++ map f Ixl by (map++)
= map f ( r e v e r s e x s ) ++ [f XI by (map. 11, (map.2)
Using the induction hypothesis, we can make one more step,
= r e v e r s e (map f xs) ++ [f XI
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.
f i l t e r and f o l d r . 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 x s and ys:
map (f . g ) x s = (map f . map g) x s
( f i l t e r p . map f ) x s = (map f . f i l t e r ( p . f ) ) x s
map f ( r e v e r s e x s ) = r e v e r s e (map f x s )
map f (ys++zs) = map f y s ++ map f z s
We have seen that using the general functions map, f i l t e r 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 function,
and instead simply use one of these theorems.
( Exercises 7
10.39 Prove that for all y s and z s the equation
f o l d r f st (xs++ys) = f ( f o l d r f st x s ) ( f o l d r f st ys)
Verification and general functions 201
concat = f o l d r (++) [I
as the definition of concat.
10.42 Prove that for all finite lists xs, and functions f ,
f i l t e r p ( f i l t e r q xs) = f i l t e r (p &&& q) x s
[ 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, f i l t e r
and f o l d r within our programs, and that we can indeed treat functions as 'first-class
citizens' of our programming language. A consequence of this has bccn that we are
able to explain the definitions of some of the P i c t u r e operations first scen in Chapter I .
The main mechanisms introduced here have allowed us to create functions by apply-
ing functions or operators to fewer arguments than wc 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 whcncver the general function is reused.
Program development
11.1 The development cycle
11.2 Development in practice
In this short chapter which builds on the discussion in Chapter 4 we step back from the
details of programming in Haskell to take a more general look at the cycle of stages in
which we see a program being developed. Although some of the remarks are specific to
Haskell, most are general, and would applyto developing a program in any programming
language.
We include a table giving hints about how to proceed in the four steps of understand-
ing, design, implementation and reflection, which owes much to Polya's approach to
problem solving in mathematics (Polya 1988). We conclude the chapter by looking at
some Haskell illustrations of the general advice given earlier.
(
1 1The development cycle
We can see programs being developed in a cycle.
Understand
the problem
Write the
solution
The development cycle 203
First, we have to understand the programming problem that we are trying to solve
- we spent some time talking about this in Chapter 4. Once we have done this we can
try to plan or design how we might approach the problem, using all the resources of
the programming language, its libraries and also the programs that we have already
written. Again in Chapter 4 we argued that we can make considerable progress at this
stage before we actually start to write programs, which is the next step in the cycle.
Once the program is complete we can reflect or look back and see how well we have
achieved our goal: we might test the program - as outlined in Chapter 4-or we might at
this stage try to prove various properties of the programs we have written. We can also
at this point look back at the original problem - was that in fact what the user wanted
to solve, or in the light of seeing a running program does the user want to change the
specification of the problem?
This clockwise cycle of stages is a simplified model of the way that software is built
in practice, from small exercise programs to large-scale industrial projects. Even for
the sort of programs we are writing here, reflecting on what we are doing is a very
important activity. and this is emphasized by the dotted arrows in the cycle diagram.
Especially when we are learning to program it is very good to get into the habit of
criticizing our own and other people's programs. Sometimes, indeed, we find that after
we have written a program we have gained so much deeper an understanding of the
problem and the ways that we might solve i t that we throw away our first solution and
rewrite it from scratch in order to clean it up and to reflect our better understanding.
It is hard to give general advice about how to write programs, but some of the most
important ideas are contained in the table in Figure 1 1.1. The advice contained there
is strongly influenced by Polya's approach to problem solving in mathematics, and his
How To Solve It (Polya 1988) contains a wealth of suggestions about how to go about
looking for solutions to problems, many of which carry over to programming examples.
The suggestion of error logging is an integral part of Humphrey's P e r s o ~ ? dS o j t ~ w t - e
Process (Humphrey 1996).
In the next section we illustratc some of the points in the development cycle using
Haskell programming examples.
204 Program development
Designing a solution -
Writing a program
To write a program you need to be aware of the resources that your programming
language provides. You also need to follow the informal design or plan.
o Haskell has a substantial number of library fimctions which support programming
over lists. Some of these are general polymorphic higher-order functions which can be
used in a large variety of situations. Try to use these if you can. (cont.)
o We shall see that over other data types we can define similar general functions. It is
usually easier to use these functions than to write a solution from scratch.
o You write your own general functions by abstracting away from the particular.
Specifically, the particular - like multiplying by two - can be turned into a function
which becomes a parameter to the general function (such as map).
o Most languages allow you to make definitions with different cases; Haskell also
provides pattern matching. which selects parts of an ob,ject as well as distinguishing
cases.
o Recursion is a general strategy for designing programs over data types like lists and
numbers. To define a recursive function f at argument x you need to ask 'bvhnt $1 l l t r t l
the V L I ~ L I Po f f tit . . . ?'.
o List comprehensions provide an expressive notation for lists.
o You may need to introduce other functions as you begin to write your definitions.
These might appear in where clauses or at the top level of the program.
o If you cannot define the function you need to, try a sinipler one. A solution to this
might be a model for what you want, or could be used in the definition of the linal
function.
Reflection
Look~ngback on what you have done might affect your program, i t \ de\ign or indccd
the \pecificat~onof the problcm itclf.
o Can you test your solution? You need to think of the testing groups over which the
program should show similar behaviour, as well as looking hard at any special cases.
o If your testing reveals errors or 'bugs', try to find their source. Are error\ duc
to accidental mistakes'? problems in understanding how the Haskell language works?
miwnderstanding how to solve the problem? misunderstanding the problem it\elf? or
some other reason'?
o You can learn from the errors you have made; try keeping a log of all the crrors that
you make. and the reason for them. This should help you not to repeat them.
o Can you prove that your program does what it should? If not, you can ask why this
is, and whether i t points to errors in the program or the design.
o Suppose you were asked to write the same program again. How would you do it
differently'?
o Suppose you were asked to modify or extend the program. How easy would that
be? If it is difficult, can you think how you might have designed or written the solution
differently to accommodate changes more readily'?
o Does the program run in reasonable time'? If not, can you see where the bottlenecks
are'? Can you see how to modify the program to improve its performance'?
(
1 1Development in practice
This section looks at the design and programming advice from Chapter 4 and Figure
I 1.1 by means of a series of programming examples.
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 I we should solve the more general problem of defining lists beginning
at an arbitrary value. We therefore define [m . . n] :
[m ., nl
I m>n = [I
I otherwise = m . [m+l .. n]
but ( . .3) has the disadvantage that it is substantially less efficient than ( . . 2 ) , a topic
we pick up in Chapter 19.
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
getLine : : Int -> [Word] -> Line
One way of approaching the problem is first to think of identifying palindromes where
punctuation and capitalization are not considered, such as "ABBA". We might solve this
by
Development in practice 207
for instance, but note that there are at least two other different ways we might implement
the function simplePalCheck. Once we have this function we can then modify it to
solve the original problem. Alternatively we can use this solution to a simplified
problem in the full solution:
palcheck = simplePalCheck . clean
where
clean : : String -> String
puts all capitals into small letters and removes punctuation. We look at this in the next
section.
Design choices
The clean function combines mapping (capitals to smalls) and filtering (removing
punctuation) and so can be solved thus
clean = map tosmall . filter notPunct (clean. I )
Auxiliary functions
Suppose we are asked to define when one string is a subsequence of another. By that
we mean that the characters ofthe 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 detine is
subseq : : String -> String -> Bool
and we try to define this by recursion. Starting with the cases of the empty string,
subseq [I - = True
subseq ( - : -) [I = False
This latter is not a recursive call to the function we are defining, so we have to say
f Exercises 1
11.1 Give a recursive definition of the range
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, 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. Thi4 is something that we d o by continually revisiting the Picture
case study.
Overloading and type
classes
12.1 Why overloading?
12.2 Introducing classes
12.3 Signatures and instances
12.4 A tour of the built-in Haskell classes
12.5 Types and classes
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. Overloaded functions like equality, + and show can be used at
a variety of types, but with different definitions being used at 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 - so-
called instances of the class. We will also see that there is a form of inheritance between
type classes, which is related to the inheritance of object-oriented programming. We take
this up again in Chapter 16 below.
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 (Peyton Jones
and Hughes 1998) for a full exposition.
Why overloading? 21 1
Why overloading?
This section looks at the reason for including overloading in Haskell; we do this by
looking at a scenario.
Suppose that Haskell did not have overloading, and that we wanted to check whether
a particular element is a member of a list of type Bool. We would define a function like
One way out of this problem is to make the equality function a parameter of a general
function
but this gives too much generality in a sense, because it can be used with uny parameter
of type a -> a -> Bool rather than just an equality check. Also in this case the
parameter has to be written down explicitly each time the function elemGen is used, as
in
where the type a has to be restricted to those types 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 = = ~ and ~ t SO on. This argument
holds particularly for numeric operators, where it is more than tiresome to have to
write + ~ *~ ~t l, and ~ so
~ on.
t
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.
212 Overloading and type classes
but this type only holds 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 class. For instance, the set of types over which == is defined is the equality
class, Eq.
Members of a type class are called its instances. Built-in instances of E q include
the base types I n t , F l o a t , Bool, Char. Other instances are given by tuples and lists
built from types which are themselves instances of Eq; examples include the types
( I n t ,Bool) and [ [Char] 1.
Not all types will necessarily carry an equality; we may choose not to define one. for
reasons of information hiding, or there may be no natural way of defining an equality
on a particular type. For example, function types like I n t -> I n t are not instances of
Eq, since there is no algorithm which will decide whether two functions over I n t have
the same behaviour.
It is unfortunate that the term instance is used in two quite different ways in Haskell.
We talked in Section 5.7 of a type t 1 being an instance of a type t 2 , when we can
substitute for a type variable in t 2 to give ti. Here we have talked about a typc bcing
an instance of a c1~1.s.s.
decides whether three integers are equal. If we cxamine the definition itself, it contains
nothing which is specific to integers; the only constraint it makes is that m, n and p are
compared for equality. Their type can be a for any a in the type L'INSSEq. This gives
a l l E q u a l a most general type thus:
a l l E q u a l : : Eq a => a -> a -> a -> Bool
a l l E q u a l m n p = (rn==n) && (n==p)
Introducing classes 213
The part before the => is called the context. We can read the type as saying that
if the type a is in the class Eq - that is, if == is defined over the type a - then
a l l E q u a 1 has type a -> a -> a -> Bool
since both Char and ( I n t ,Bool) belong to Eq, among many olher types. What happens
if we break this constraint by trying to compare functions for equality? If we define
s u c : : I n t -> I n t
SUC = (+I)
which conveys the fact that ( I n t -> I n t ) is not in the Eq class, hccause it is not an
instance of that class.
Many of the functions we have defined already use equality in an overloaded way. We
can use the Hugs system to deduce the most general type of a function. such as the
books fiinction from the library database of Section 5.6, by commenting out its type
declaration in the script, thus
:t y p e books
which is perhaps a surprise at first. This is less so if we rewrite the definition with
books renamed lookupFirst, because it looks up all the pairs with a particular first
part, and returns their corresponding second parts. Here it is with its variables renamed
as well
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 E q a. Finally from Section 5.6, as we saw for
books,
( 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 are defined over them. One way
we can think of a class is as an adjective: any particular 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 sections we
explain how classes and instances are defined in general, and explore the consequences
of classes for programming in Haskell.
( Exercises 1
12.1 How would you define the 'not equal' operation. /=, from equality. ==? What
is the type of /=?
12.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 i n xs. What is the type of your
function? How could you use numEqual to define member'?
12.3 Define functions
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.
Signatures and instances 215
Declaring a class
As we saw earlier, a class is introduced by a declaration like:
The declaration introduces the name of the class, Visible,and then follows asignature.
that is a list of names and their types. Any type a in the Visible class must carry the
two functions in the signature:
Visible things can be viewed, using the tostring function, and we can give an estimate
of their size: the size of a list might be its length, while a Boolean might have size one.
The general form of a class definition will be:
describes how Bool is an instance of the equality class. The declarations that numeric
types like Int and Float are in the equality class (and indeed other built-in classes)
involve the appropriate primitive equality functions supplied by the implementation.
Although we have called the class Eq the equality class, there is no requirement that
the == function we define has any of the usual properties of equality apart from having
216 Overloading and type classes
the same type as equality. It is up to the user to ensure that he or she makes sensible
definitions, and documents them adequately.
Taking up our other example, we might say
This shows how characters can be turned into strings - by making them into strings of
length one - and gives a measure of their size. We can also make Boo1 an instance.
thus:
Suppose the type a is visible: this means that we can estimate the size of a value in a,
and turn a value into a string. If presented with a list of values of type a, we can use
the tostring and size on a to define those functions over [a], so we can declare the
following instance
To turn a list of a into a String, we turn each element of the list into a string (map
tostring) and then we concatenate the results, using concat. In a similar way we
can estimate the size of a list of a: we take the size of each object (map size), and add
one to the total of these sizes by f oldr (+) 1.
On the right-hand sides of these definitions we use tostring and size over the type
a;this shows that we need the context which says that a is a Visible type.
There are some limitations to what can be declared as an instance, in other words
on what can appear after the => (if any) in an instance declaration. This must either
be a base type like Int, or consist of a type former (or constructor) like [ . . .I or
( . . . , . . . ) 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 report (Peyton Jones and Hughes 1998). We shall explore
more complex examples in the next part of the book, after we have introduced our own
type constructors.
Signatures and instances 217
Default definitions
To return to our example of equality, the Haskell equality class is i n fact defined by
class E q a where
(==I, (/=) :: a -> a -> Bool
x /= y = not (x==y)
== 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 are 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 t o 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
x / = y -- . . . our definition ..
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 E q we would give the top-level definition
x /= y = not ( X == y)
and will be effective over all types which carry the == operation.
There are some situations when it is better to give default definitions, which can
be overridden, rather than top-level definitions, which cannot. Over the nu~nerical
types, for instance, an implementation may well supply all the operations as hardware
instructions, which will be much more efficient than the default definitions.
Derived classes
Functions and instances can depend upon types being i n 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
For a type a to be in the class Ord, we must supply over a detinitions 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,
We will explain the type Ordering and the function compare in Section 12.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 operations
of Eq; inheritance is one of the central ideas of object-oriented programming.
Multiple constraints
In the contexts we have seen s o 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 Visible; given
the instance declaration on page 2 16, this will hold if a is visible. We therefore have
showing that a must be in both the classes Ord and Visible. 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) I . 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 Visible 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 Visible.
In this particular case, the class declaration contains an empty signature. To be in
OrdVis, the type a must simply be in the classes Ord and Visible. We could then
modify the type of vSort to say
The situation when a class is built on top of two or more classes is called multiple
inheritance: this has consequences for programming style, explored in Section 14.6.
( Summary )
This section has explained the basic details of the class mechanism in Haskell. We
have seen that a class definition specities a signature, and that in defining an instance
of a class we must provide definitions of each of the operations of the signature. These
detinitions 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
12.4 How would you make Bool, pair types, (a,b), and triple types, (a,b , c ) , into
Visible types?
12.5 Write a function to convert an integer into a String, and hence show how Int
can be an instance of Visible.
where pairs and lists should be ordered lexicographically, like the words in a
dictionary.
220 Overloading and type classes
Haskell contains a number of built-in classes, which we briefly introduce in this section.
Many of the classes are numeric, and are built to deal with overloading of the numerical
operations over integers, floating-point reals, complex numbers and rationals (that is
integer fractions like 7). Rather than give complete details of the numeric types. we
give an exposition of their major features.
Equality: E q
Equality was described above; to recap, we define it by
class Eq a where
(==I, (/=) :: a-> a -> Bool
x /= y = not (x==y)
x == y = not (x/=y)
Ordering: Ord
Sin~ilarly.we build the ordered class on Eq:
class (Eq a) => Ord a where
compare : : a -> a -> Ordering
(<) , (<=), ( > = I , (>) : : a -> a -> Bool
max, min : : a - > a -> a
The type Ordering contains three values LT, EQ and GT, which represent the three
possible outcomes from comparing two elements in the ordering. We shall see how the
type Ordering is detined formally in Chapter 14, page 243.
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 neccwary. Indeed. we
see this in the dehult definition of compare from ==and <=, where two tests are needed
to reach the results LT and GT.
compare x y
I x = = y = EQ
I x<=y = LT
1 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
There are dcfault 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,
A tour of the built-in Haskell classes 221
max x y
I x>=y = X
I otherwise = y
min x y
I x < = y = X
1 otherwise = y
Most Haskell types belong to theve equality and ordering classes: among the exceptions
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,81 using the enumeration expression
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
toEnum : : Int -> a
fromEnum : : a -> Int
enumFrom : : a -> [a] -- [n .. 1
enumFromThen : : a -> a -> [a] -- [n,m .. 1
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. In the case of Char these conversion functions are
also known as ord and chr,where these specializations are given by the definitions:
ord : : Char -> Int
ord = fromEnum
c l a s s Bounded a where
minBound, maxBound : : a
and the two values give the minimum and maximum values in these types. The types
I n t , Char, Bool, Ordering belong to this class.
t y p e ShowS = S t r i n g -> S t r i n g
c l a s s Show a where
showsPrec : : I n t -> a -> Shows
show : : a -> S t r i n g
showList : : [a] -> ShowS
The function showsPrec supports flexible and efficient conversion of large data values,
but in an introductory context, the function
show : : a -> S t r i n g
which converts a value into a string is all that is needed. The class contains default
definitions of showsPrec from show and vice versa. Further details about how to
exploit the subtleties of showsPrec can be found in Hudak, Fasel and Peterson (1997).
Most types belong to the class Show; even if values of the type in question cannot be
shown fully, a textutal representation of some sort is given. A function, for example,
will be shown as <<function>>.For other types, example instance declarations might
be
The result of a r e a d may not be properly defined: there needs to be exactly one object
of the required type in the input string (which may optionally also contain whitespace
or nested comments); in any other case the read will fail with an error. More details
of how strings are parsed in this way can be found in Section 17.5.
It is also important to see that in many cases the type of the result of the r e a d has to
be specified. since it could potentially be of any type in the class Read. For instance.
we can write
which indicates that in this case we require the result of the read to be an I n t .
The class Read complements Show, since strings produced by show are usually
readable by read. Many types can be read, but exclusions include function types.
The fixed precision integers, I n t , and the full precision integers, I n t e g e r , which
represent d l integers faithfully.
The floating-point numbers, F l o a t , and thedouble-precision floating-point numbers.
Double.
Rational numbers, that is fractions, represented as ratios of integers; built-in is the
type R a t i o n a l of I n t e g e r fractions.
Complex numbers, which can be built over other types such as F l o a t .
The design also required that the usual operations like + and / and literals such as 23
and 5 7 . 4 would be overloaded. For instance, I n t and I n t e g e r will carry identical
operation\' and have identical literals, as indeed will F l o a t 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 exprc\sion, thus:
(2+3) : : I n t
The Haskell report (Peyton Jones and Hughes 1998) discusses a mechanism by which
a default type can be given to numeric expressions.
Overloading of numeric functions is achieved by defining a collection of classes. Full
details of these can be found in the Haskell report (Peyton Jones and Hughes 1998).
and in the standard prelude, Prelude .hs; a brief introduction follows here.
The base class to which all numeric types belong is Num, which has the signature
I Apart fro111(dr)coding of Char, take,d r o p and so forth
224 Overloading and type classes
- Y = x + negate y
f romInt = fromIntegra1
Thi\ signature has the effect that all numeric types carry equality and show functions.
together with addition, subtraction, n~ultiplicationand related operations. It is also
possible to convert an I n t or and I n t e g e r into a value of any numeric type.
Integer literals are of any numeric type, so that, for example
The integer types belong to the class I n t e g r a l among whose signature functions are
q u o t , rem : : a -> a -> a
d i v , 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 F r a c t i o n a l class,
2 . 3 : : F r a c t i o n a l a => a
which extends Num with fractional division and reciprocal,
c l a s s (Num a ) => F r a c t i o n a l a where
(/I : : a -> a -> a
recip : : a -> a
f romRationa1 : : Rational -> a
recip x = l / x
The floating-point numbers in F l o a t and Double belong to the class Floating. which
carries the 'mathematical' functions. A part of its signature follows,
c l a s s ( F r a c t i o n a l a ) => F l o a t i n g a where
pi :: a
exp, l o g , s q r t : : a -> a
(**I, logBase : : a -> a -> a
s i n , cos, t a n : : 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
docunlentation.
Types and classes 225
Exercises
12.9 Investigate theHaskell definition of '<' on the types Bool and (t1 ,t2,. . . ,tk) .
12.1 1 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, since Bool -> Bool is not of the right form
tbr an instance declaration.)
Types a n d classes
This section discusses the relationship between Haskell type classes and the classes of
object-oriented programming; it can be omitted on first reading.
The type system of Haskell can be seen as giving monomorphic types to functions.
Polymorphic types like
show : : Show a => a -> String
which involve type classes can be seen as shorthand for collections of typings, such as
in its interface. The class ShowType would have Bool and Char among its sub-classes
(or sub-types). This would allow us to write values like
' InC++ terninology this would be an abstract base class, with Bool etc. inheriting and being forced to
imple~iientthe oprations of that class.
226 Overloading and type classes
so that it can be applied to elements of [Bool], [Char] and so on, but not to hetero-
geneous lists like [True, ' N ' , F a l s e ] 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 i n Haskell it is not
necessary to make the instance declaration a part of the type definition itself. This has
the effect of allowing p o s t hoc. extensions to the operations supported by a type, in a
way which is not poccible for a class i n Java.
Summary
This chapter has shown how names such as r e a d 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 c l a s s 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 i n s t a n c e 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
In the examples, it can be seen that member can only be used over types i n 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, and concluded with a discussion of the relationship
between the type classes of Haskell and the classes of object-oriented programming. In
the final part of the book we shall revisit classes and see how they are used to structure
larger-scale systems.
Checking types
13.1 Monomorphic type checking
13.2 Polymorphic type checking
13.3 Type checking and classes
Every value in Haskell has a defined type, which might be monomorphic, polymorphic,
or involve one or more type class constraints in a context. For example,
'w' :: Char
flip : : (a -> b -> c) -> (b -> a -> c)
elem :: Eq a => a -> [a] -> Boo1
Strong typing means that we can check whether or not expressions we wish to evaluate
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.
Beyond this, types are a valuable form of program documentation: when we look at
a definition, the first relevant piece of information about it is its type, since this explains
how it is to be used. In the case of a function, we can read off from its type the types of
values to which it has to be applied, and also the type of the result of applying it.
Types are also useful in locating functions in a library. Suppose we want to define a
function to remove the duplicate elements from a list, transforming [2,3,2,1,3,41 to
[2,3,1,41, for instance. Such a function will have type
A search of the standard prelude Prelude .hs and the library L i s t . hs reveals just one
function of this type, namely nub, which has exactly the effect we seek. 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 'handle' on the functions in a library.
In 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
228 Checking types
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 looking 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.
+I n t '
I n t -> I n t -> I n t
lengthoar : : [Char] -> I n t
We look first at the way that we type-check expressions, and then look at how
definitions are type-checked.
Expressions
In general, an expres\ion ic either a literal, a variable or a con\tant or it is built up by
apply~nga function to ronie ~lrguments,which are themselve\ expressions.
The cace 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, ' : ', thu\: True : [False]. Also, operators and the i f . . then
. . . e l s e construct act in exactly the wme way as function\, albeit with a different
syntax.
The rule for type checking a function application is set out in the following diagram.
where we w e that a function of type s -> t must be applied to an argument of type s.
A properly typcd application re\ults in an exprecrion of type t .
f must have a
function type e must have
the result ,
has type t
Monomorphic type checking 229
We now look at two examples. First we take o r d ' c ' +Int 3,nt, a correctly typed
expression of type I n t ,
0
0
0
Int Int -> Int -> Int Int
The application of o r d to ' c ' results in an expression of type I n t . The second argument
to +Int is also an I n t , so the application is correctly typed, and gives a result of
type I n t .
If we modify the example to o r d ' c ' +I n t False, we now see a type error, since a
Boolean argument, F a l s e , is presented to an operator expecting I n t arguments, +Int.
C h a r -> I n t
.-.
..-. Char
.. L.
ord
\
-1
'c' +,,,, False
0
0
0
0
-'
+I
k
\
\
\
Argument of ~n t expected
the correct type ~ o o lgiven
Function definitions
In type-checking a monomorphic function definition such as
( f def )
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 consistent
with its type. A pattern ( p : q ) is consistent with the type [t] if p is consistent with t
and q is consistent with [t]. For example, ( 0 : ~ s )is consistent with the type CIntl,
and (x:xs) is consistent with any type of lists. The other cases of the definition are
similar.
This concludes our discussion of type checking in the monomorphic case; we turn
to polymorphism next.
( Exercise I
13.1 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
I x>O = True
I otherwise = 37
Check your answers by typing each definition into a Haskell script, and loading
the script into Hugs. Remember that you can use :type to give the type of an
expression.
Polymorphism
We are familiar with functions like
whose types are polymorphic, but how should we understand the type variable a in this
type? We can see (length) as shorthand for saying that length has a set of types,
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
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 interpretation, 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 9.2.We consider some further examples now.
( Examples 1
1. Consider the definition
The argument o f f is a pair, and we consider separately what constraints there are on
the types o f 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' . . yl, which denotes
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 :
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.
232 Checking types
g (m,zs) = m + l e n g t h zs
First, we see that zs will have to be of type [bl, and also that the result is an Int. This
forces + to be used at I n t , and so forces m to have type I n t , giving the result
g : : ( I n t , Cbl) -> I n t
the output 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 ( I n t , [bl 1. We therefore have to look for types whichmeet
both these descriptions. We will now look at this general topic, returning to the example
in the course of this dicussion.
Unification
How are we to describe the types which meet the two descriptions ( a , [Char] ) and
(Int , Cbl)?
Polymorphic type checking 233
As sets of types, we look for the intersection of the sets given by ( a , [Char] ) and
( I n t , Cbl ) . How can wc work out a description of this intersection'? Before we do
this, we revise and introduce some tern~inology.
Recall that an instance of a type is given by replacing a type variable or variables by
type expressions. A type expression is a common instance of two type exprcssions if it is
an instance of each expression. The most general common instance of two expressions
is a common instance mgci with the property that every other common instance is an
instance of mgci.
Now we can dcicribe the intersection of the sets given by two type expressions. It is
called the unification of the two, which is the most general common instance of the
two type expressions.
(Int , [Char])
with a single type resulting. This givcs the function h the following type
h : : ( I n t , [Char]) -> Int
and this completes the discussion of example 3.
Unification need not rc\ult i n a monotype. In the example of unifying the types ( a , [a] )
and ( [bl ,c ) ,
the result is the type ( Cbl , C [b] I ) . This is because the expression ( a , [a] ) constrains
the type to have in its second component a list of elements of the first component type,
while the expression ( [bl ,c ) constrains its first component t o be a list. Thus satisfying
the two gives the type ( [bl , [ [bl I ) .
In the last example, note that there are many common instances of the two type
expressions, including ( [Booll , [ [Bool] 1 ) and ( C [ c l ] , [ C Ccl I I 1, but neither of
these examples is the unifier, since ( [bl , C [b] 1) is not an instance of either of them.
On the other hand, they are each instances of ( [b] , C [b] I ) , 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 [ I n t l -> CIntl and
a -> [a].
234 Checking types
a becomes Int
-> [Int]
a becomes [Intl
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.1, function application is central to expression formation. This
means that type checking also hinges on function applications.
As in the monomorphic case, we can use this discussion of typing and function appli-
cation in explaining type checking all aspects of expressions. We now look at another
example, before examining a more technical aspect of type checking.
4. f o l d r again
In Section 9.3 we introduced the f o l d r function
f o l d r f s [I = s ( f o l d r . 1)
f o l d r f s (x:xs) = f x ( f o l d r f s xs) ( f o l d r .2)
In fact, the most general type of f o l d r is more general than this. Suppose that the
starting value has type b and the elements of the list are of type a
f o l d r : : ( . . . -> . . . -> ...) -> b -> [a] -> . . .
P a
foldr f s [ I = s / b
s is the result of the ti rst equation, and so the result type of the f o l d r function itself
will be b, the type of s
Finally. the result of the second equation is an application of f; this result must have
the same result type as the f o l d r itself, b.
With this insight about the type o f f o l d r we were able to scc that f o l d r could be used
to define another whole cohort of list functions, such as an insertion sort,
in which i n s has the type Ord a => a -> [a1 -> [a]
was a correct type, but this would mean that funny would have all the instance types
funny : : [ I n t ] -> I n t
funny : : [ [Char] ] -> I n t
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 ( e x p r ) ? Taking ( e x p r ) first, we have a polymorphic definition of the form
[I : : [a] and an expression in which [I occurs twice; the first occurrence is at
[Bool] , the second at [ I n t ] . 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.
Polymorphic type checking 237
On the other hand, how is the delinition of (funny) disallowed? When wc type
check the use of a variable we will not treat each instance as being of an independent
type. Suppose we begin with no constraint on xs, so xs : :t , say. The first occurrence ol'
xs forces xs : : [Bool] , the second requires xs : : [ I n t ] ; these two constraints cannot
be satisfied simultaneously, and thus the definition (funny) fails to type check.
The crucial point to remember rroni this example is that the definition of a function
is not permitted to force any of its arguments to he polyn~orphic.
Function definitions
In lype checking a function definition like (f d e f ) on page 229 above we have to obcy
rules similar to the monomorphic casc.
We take up a final aspect of type checking - the impact of type classes - in rhc next
scction.
f Exercises 7
13.2 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.
( I n t -> b)
( a -> Bool)
13.7 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.8 [Harder] Recalling the definitions of curry and uncurry from Section 10.7.
what are the types of
curry id
uncurry id
curry (curry id)
uncurry (uncurry id)
uncurry curry
curry uncurry
curry curry
13.9 [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.
Classes in Haskell restrict the use of some functions, such as ==, to types i n the class
over which they are defined, in this case Eq. These restrictions are apparent in the
contexts which appear in some types. For instance, if we define
member [I y = False
member (x:xs) y = (x==y) I I 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 overloading
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
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
(Eq b , Ord b) ( c t x . 2)
so that any instance of Ord is automatically an instance of Eq; this means that we can
simplify ( c t x . 2) to
Ord b
240 Checking types
The type checking of functions which use thcsc: overloaded functions will propagate
and combine the contexts as we have seen above.
We have seen informally how the Haskell type system accommodates type checking
for the overloaded names which belong to type classes. A more thorough overview of
the technical aspects of this, including a discussion of the 'monomorphism restriction'
which needs to be placed on certain polymorphic bindings, is to be found i n the Hnskell
98 report (Peyton Jones and Hughes 1998).
( Exercises 7
13.10 Give the type of each of the individual conditional equations which follow, and
discuss the type of the function which together they define.
13.11 Define a polymorphic sorting function, and show how its type is derived from
the type of the ordering relation
13.1 2 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
Type checking and classes 241
( Summary
The chapter explained how type checking of expressions and definitions is perfornled
in Haskell. Initially this was explored in the monomorphic case, and then expanded to
deal with polymorphism. In that case we saw type checking as a process of extracting
and consolidating constraints, the latter being given by unification of type expressions
which contain type variables. We concluded by examining how to manage contexts in
types, and thus how overloading is handled in the Haskell type system.
( Chapter I 4 )
Algebraic types
14.1 Introducing algebraic types
14.2 Recursive algebraic types
14.3 Polymorphic algebraic types
14.4 Case study: program errors
14.5 Design with algebraic data types
14.6 Algebraic types and type classes
14.7 Reasoning about algebraic types
So far in our discussion of Haskell we have been able to model entities using
composite types: tuple types, ( t i ,t2,.. . , t n ) ; list types, [ti];and function types,
( t i -> t 2 ) ; where ti, . .., tn are themselves types.
This gives a wide choice of types and we have seen quite complex structures, like an
index for a document, represented by the appropriate combination of types: in the
index example, [: ( [ I n t l , [Char] ) I was used.
However, there are other types which are difficult to model using the constructs we
have seen so far. Examples include
All these types can be modelled by Haskell algebraic types, which form the subject of
this chapter.
Introducing algebraic types 243
Enumerated types
The simplest sort of algebraic type is defined by enumerating the elements of the type.
For instance,
introduces two types. The type Temp has two members, Cold and Hot, and Season has
four members. More formally, Cold and Hot are called the constructors of the type
Temp.
To define functions over these types we use pattern matching: we can match against
either a literal or a variable. To describe the (British!) weather we might say
Pattern matching is sequential; the first pattern to match an argument will be used. This
means that the British weather is only hot in the summer, and it is cold the rest of the
year. The built-in Boolean type is defined by
d a t a Boo1 = F a l s e I True
d a t a Ordering = LT I EQ I GT
244 Algebraic types
As we have seen, pattern matching is used to define functions over algebraic types. We
can use it to define equality over Temp, for instance,
Cold == Cold = True
Hot == Hot = True
- -- - = False
to put Temp into the equality class Eq.
It would be tiresome to have to give a definition of equality for every new type which
we introduce, and so the Haskell system can be made to generate definitions of ==,
ordering, enumeration and text functions automatically. We discuss the details of this
at the end of this section, after looking at some more examples.
Product types
Instead of using a tuple we can define a type with a number of components, oftcn called
a product type, as an algebraic type. An example might be
data People = Person Name Age (People)
where Name is n 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 value$; 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. A general element of type
People has the form Person st n, and we can use this pattern on the Icft-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, since Int belongs to the Show class).
For instance.
showPerson (Person "Electric Aunt Jemima" 77)
= "Electric Aunt Jemima -- 77"
In this example, the type has a single constructor, Person,which is binary bccause i t
takes two elements to form a value of type People. For the enumerated types Temp
and Season the constructors are called nullary (or 0-ur-y)as they take no arguments.
Introducing algebraic types 245
The constructors introduced by algebraic type definitions can be used just like
functions, so that Person s t n is the result of applying the function Person to the
arguments s t and n; we can interpret the definition (People) as giving the type of the
constructor, here
Person : : Name -> Age -> People
An alternative definition of the type of people is given by the type synonym
t y p e People = (Name,Age)
The advantages of using an algebraic type are threefold.
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.
The elements are more compact, and so definitions will be shorter.
Using a tuple, especially a pair, allows us to reuse many polymorphic functions such
as f s t , snd and unzip over tuple types; this will not be the case for the algebraic
tYPe.
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 cqually well with unary constructors, so we might say
d a t a Age = Years I n t
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 I n t directly over Age.
We can use the same name, for instance Person, for both the type and the constructor
of a type, as in the definition
d a t a Person = Person Name Age
We choose not to do this, as using the same name for two related but different objects can
easily lead to confusion. but it is an idiom used by a number of Haskell programmers.
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
alternatives are given by the type
246 Algebraic types
and show values of the type. The same applies to Shape, except that we cannot
enumerate shapes; being in Enum can only be derived for enumerated types such as
Season.
We are not forced to use the derived definitions; we can give our own instances.
so that, for example, all circles of negative radius are made equal. The definition of
showPerson above could also form a model for making People an instance of the type
class Show.
14.1 Redefine the function weather: : Season -> Temp so that a guard or an i f
. . . is used rather than pattern matching. Which of the definitions is preferable
in your opinion?
14.2 Define the type of months as a Haskell algebraic type. Give a function which
takes a month to its appropriate season - in doing this you might want to use the
ordering on the type, which is derived as explained above.
14.3 What would be the weather function for New Zealand, which is on a similar
latitude to Britain, but in the Southern Hemisphere? What would be the definition
for Brazil, which is crossed by the Equator?
14.4 Define a function to give the length of the perimeter of a geometrical shape, of
type Shape. What is the type of this function?
14.5 Add an extraconstructor to Shape for triangles, and extend the functions isRound.
a r e a and perimeter to include triangles.
14.6 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.
14.7 Investigate the derived definitions for Temp and Shape: what form do the
orderings and the show functions take, for example?
14.8 Define == over Shape so that all circles of negative radius are equated. How
would you treat rectangles with negative sides?
14.9 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:
Introducing algebraic types 249
Similarly, a tree is either nil or is given by combining a value and two sub-trees. For
example, the number 1 2 and the trees in Figure 14.2 are assembled to give the tree in
Figure 14.1. As a Haskell type we say
d a t a NTree = NilT 1
Node I r t NTree NTree
Finally, we have already used the type of lists: a list is either empty ( [I ) 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.
d a t a Expr = Lit Int 1
Add Expr Expr I
Sub Expr Expr
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.
e v a l : : Expr -> I n t
e v a l ( L i t n) = n
e v a l (Add e l e2) = ( e v a l e l ) + ( e v a l e2)
e v a l (Sub e l e2) = ( e v a l e l ) - ( e v a l e2)
the result.
show ( L i t n) = show n
show (Add e l e2)
= 11(11++ show el ++ "+I1 ++ show e2 ++ " ) "
show (Sub e l e2)
= (I1++ show e l ++ "-" ++ show e2 ++ ") "
as does the function to calculate the number of operators in an expression; we leave this
as an exercise. Other exercises at the end of the section look at a different representation
of expressions for which a separate type is used to represent the different possible
operators. Next, we look at another recursive algebraic type, but after that we return
to Expr and give an example of a non-primitive-recursive definition of a function to
rearrange expressions in a particular way.
Trees of integers
Trees of' integers like that in Figure 14.1 can be modelled by the type
d a t a NTree = NilT I
Node I n t NTree NTree
The null tree is given by NilT, and the trees in Figure 14.2 by
252 Algebraic types
sumTree NilT = 0
sumTree (Node n ti t2) = n + sumTree tl + sumTree t2
depth NilT = 0
depth (Node n ti t2) = 1 + max (depth tl) (depth t 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.
occurs NilT p = 0
occurs (Node n tl t2) p
I n==p = 1 + occurs ti p + occurs t2 p
I otherwise = occurs ti p + occurs t2 p
The exercises at the end of the section give a number of other examples of functions
defined over trees using primitive recursion. We next look at aparticular example 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, decide 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 the following table:
Recursive algebraic types 253
The problem is that in transforming (AddL) to (AddR) we may produce another pattcrn
we are looking for at the top level: this is precisely what happens when ( AddExL) i \
translbrnled to (AddExR). We therefore have to call the fimction cigain on the re\ult of
the rearrangement
assoc : : Expr -> Expr
The other cases in the definition make sure that the parts of an expression are rearranged
as they should be.
assoc (Add e l e2)
= Add (assoc e l ) (assoc e2) (Add. 2)
assoc (Sub e l e2)
= Sub (assoc e l ) (assoc e2)
assoc ( L i t n)
= Lit n
The equation (Add. 2) will only be applied to the cases where (Add. 1) does not apply
- this is when e l is either a Sub or a L i t 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 terminate 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.
254 Algebraic types
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:
data P e r s o n = A d u l t Name Address Biog I
C h i l d Name
d a t a Biog = P a r e n t S t r i n g [Person] I
NonParent S t r i n g
Recursive algebraic types 255
In the case of a parent, the biography contains some text, as well as a list of their
children, as elements of the type Person.
Suppose that we want to define afunction which shows informationabout a person 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:
showPerson (Adult nm ad b i o )
= show nm ++ show ad ++ showBiog b i o
...
showBiog (Parent s t p e r l i s t )
= st ++ concat (map showPerson p e r l i s t )
Exercises
e v a l ( L i t 67)
e v a l (Add (Sub ( L i t 3) ( L i t I ) ) ( L i t 3 ) )
show (Add ( L i t 67) ( L i t (-34)))
s i z e : : Expr -> I n t
d a t a Expr = L i t Int I
Op Ops Expr Expr
Show how the functions eval, show and s i z e 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.19 Give line-by-line calculations of
14.20 Complete the redefinition of functions over Expr after it has been defined using
the infix constructors :+ : and : - : .
14.21 Detine functions to return the left- and right-hand sub-trees of an NTree.
14.23 Define functions to find the maximum and minimum values held in an NTree.
14.24 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 c o l l a p s e should enumerate the left
sub-tree, then the value at the node and finally the right sub-trcc; s o r t should
sort the elements in ascending order. For instance,
14.26 Complete the definitions of showPerson and showBiog which were left incom-
plete in the text.
14.27 It is possible to extend the type Expr so that it contains conditiotzul expressions.
I f b e l e2, where e l and e2 are expressions, and b is a Boolean expression,
a member of the type BExp,
d a t a Expr = L i t Int I
Op Ops Expr Expr I
I f BExp Expr Expr
The expression
has the value of e l 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.
Lists
The built-in type of lists can be given by a definition like
d a t a L i s t a = N i l L i s t I Cons a ( L i s t a )
d e r i v i n g (Eq,Ord,Show,Read)
where the syntax [a], [I and ':' is uscd for L i s t a, N i l L i s t and 'Cons'. Because
of this, the type of lists forms a useful paradigm fur 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.
258 Algebraic types
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:
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:
For example,
collapse (Node 12
(Node 34 Nil Nil)
(Node 3 (Node 17 Nil Nil) Nil))
= [34,12,17,3]
We shall return to trees in Section 16.7, where particular 'search' trees form a case
study.
Members of the 'union' or 'sum' type are ( L e f t x), with x: : a, and (Right y) with
y : :b. The 'name or number' type is given by E i t h e r S t r i n g I n t and
L e f t "Duke of Prunes" : : E i t h e r S t r i n g I n t
Right 33312 :: Either String I n t
e i t h e r f g ( L e f t x) = f x
e i t h e r f g (Right y) = g y
If we have a funclion f : :a -> c and we wish to apply it Lo an element of E i t h e r a b.
there is a problem: what do we do if the element is in the right-hand side of the E i t h e r
type? A simple answer is to raise an e r r o r
applyLeft : : ( a -> c) -> E i t h e r a b -> c
applyLeft f ( L e f t x) = f x
applyLeft f (Right -) = e r r o r "applyLeft a p p l i e d t o Right"
but in the next section we shall explore other ways of handling errors in more detail
260 Algebraic types
14.28 Investigate which of the functions over trees discussed in the exercises of Section
14.2 can be made polymorphic.
t w i s t : : E i t h e r a b -> E i t h e r b a
14.31 Show that any function of type a -> b can be transformed into functions of type
a -> E i t h e r b c
a -> E i t h e r c b
You might find the answer to the previous exercise useful here, if you want to
define j o i n using e i t h e r .
The trees defined in the text are hincrry: each non-nil tree has exactly two sub-
trees. We can instead define general trees with an arbitrary list of sub-trees,
thus:
attempts to divide by Lero, 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,
error :: String -> a
Dummy values
The function tail is supposed t o give the tail of a list, and it gives an error message
on an empty list:
Now, an attempt to take the tail of an?. 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.
fErr y x
I cond = Y
I 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 occurred.
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
fErr x
I cond = Nothing
I otherwise = Just (f x)
Thc rcsults 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
laybe a Maybe b
maybe n f
These two operations are illustrated in Figure 14.4, and we define them now.
The function mapMaybe transmits an error value though 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 J u s t 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
J u s t . On the other hand, if Nothing is the argument then Nothing is the result.
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
The higher-order function which achieves this is maybe, whose arguments n and f are
used in the Nothing and J u s t cases respectively.
maybe : : b -> ( a -> b) -> Maybe a -> b
maybe n f Nothing = n
maybe n f ( J u s t x) = f x
We can see the functions mapMaybe and maybe in action in the examples which follow.
In the tirst, a division by zero leads to a Nothing which passe%,through the lifting to
be trapped - 56 is therefore returned:
maybe 56 ( I + ) (mapMaybe (*3) (errDiv 9 0))
= maybe 56 ( I + ) (mapMaybe (*3) Nothing)
= maybe 56 (I+) Nothing
= 56
264 Algebraic types
In the second, a normal division returns a J u s t 9. This is multiplied by three. and the
maybe at the outer level adds one and removes the J u s t :
maybe 56 ( I + ) (mapMaybe (*3) (errDiv 9 1 ) )
= maybe 56 (I+) (mapMaybe (*3) ( J u s t 9))
= maybe 56 ( I + ) ( J u s t 27)
= 1 + 2 7
= 28
The advantage of the approach discussed here is that we can first define the system
without error handling, and afterwards add the error handling, using the mapMaybe and
maybe functions together with the modified functions to raise the error. As we have
seen numerous times already, separating a problem into two parts has made the solution
of each, and therefore the whole, more accessible.
We revisit the Maybe type in Section 18.8 where we see that it is an example of
a more general programming structure, a monad. In particular there we examine the
relationship between the function mapMaybe and the map function over lists.
14.35 Using the functions mapMaybe and maybe, or otherwise, define a function
so that p r o c e s s x s 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.36 Discuss the advantages and disadvantages of the three approaches to error
handling presented in this section.
14.37 What are the values of type Maybe (Maybe a ) ? Define a function
which composes two error-raising@nctions. How could you use mapMaybe, the
function composition operator and the squash function to define composeMaybe?
14.39 The Maybe type could be generalized to allow messages to be carried i n the
Nothing part, thus:
Design with algebraic data types 265
data Err a = OK a 1 E r r o r S t r i n g
The analysis here can also be used to describe the difference between two lists of
arbitrary type. If each item is a line of a file, the behaviour of the function is similar to
the Unix d i f f utility, which is used to give the difference between two text files.
First we have to identify the types of data involved. In the example, we have to define
d a t a Edit = ...
Next, we have to identify the different sorts of data in each of the types. Each sort of
data is given by a constructor. I n the example, we can change, copy, delete or insert
a character and delete (kill) to the end of the string. Our type definition is therefore
d a t a Edit = Change . . . 1
Copy . . . I
Delete . . . I
Insert . . . I
K i l l . ..
The ' . . . ' show that we have not yet said anything about the types of the constructors.
Finally, for each of the constructors, we need to decide what its components or
arguments are. Some of the constructors - Copy, Delete and K i l l - require no
information; the others need to indicate the new character to be inserted, so
,"
This completes the definition.
We now illustrate how other type definitions work i n a similar way, before returning to
give a solution to the 'edit distance' problem.
Design with algebraic data types 267
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 possibilities:
giving the arrival time of the customer, and the amount of time that will be needed
to serve them.
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
transform [I [I = [I
To transform the non-empty string st,to [I, we simply have to Kill it. while to
transform [I to st we have to Insert each of the characters in turn:
transform xs [I = [Kill]
transform [I ys = map Insert ys
268 Algebraic types
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 rill possibilities and choose the best of them:
transform (x:xs) (y:ys)
I x==y = Copy : transform xs ys
I otherwise = best [ Delete : transform xs (y:ys) ,
Insert y : transform (x:xs) ys ,
Change y : transform xs ys 1
How do we choose the best sequence? We choose the one with the lowest cost.
best : : [[Edit]] -> [Edit]
best[x] = x
best (x:xs)
1 cost x <= cost b = x
I otherwise = b
where
b = best xs
The cost is given by charging one for evcry operation except copy, which is equivalent
to 'leave unchanged'.
cost : : [Edit] -> Int
cost = length . filter (/=Copy)
The first four 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 adequatcl y represented the types which appear in
your informal picture of the problem.
14.40 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.41 If you knew that the records of vehicles were to bc used for comparative tests of
fuel efficiency, how would you modilji your answer to the last question'?
14.42 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 typcs that you use.
14.43 What data types might bc used to represent the objects which can be drawn
using an interactive drawing program'? To give yourself more of a challenge.
you niight like to think about grouping of objects, multiple copies of object\.
and scaling.
/'
14.44 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'?
Algebraic types and type classes 269
14.45 Write a definition which when given a list of edits and a string s t , returns the
sequence of strings given by applying the edits to st in sequence.
14.46 Give a calculation of t r a n s f orm " c a t t 1 "am". What d o you conclude about
the efficiency of the t r a n s f o r m function?
Movable objects
We start by building a class of typcs whose members are geometrical objects in two
dimensions. The operations ofthcclass are those to nwve the objects in various different
ways.
We now work through the definitions, which are illustrated in Figurc 14.5. Some
moves will be dictated by vectors, so we first define
d a t a Vector = Vec F l o a t F l o a t
The class detinition itself is
c l a s s Movable a where
move : : V e c t o r -> a -> a
r e f l e c t X : : a -> a
r e f l e c t Y : : a -> a
r o t a t e 1 8 0 : : a -> a
rotate180 = reflectX . reflectY
and it shows the ways in which an object can be moved. First it can be moved by a
vector, as i n the diagram below.
We can also reflect sn object in the x-axis (the hori~ontalaxis) or the y-axic (the
vertical), or r o t a l e d i g u r e through 180' around the origin (the point where the axe,
meet). The default definition of r o t a t e 1 8 0 works by reflecting first in the y-axi5 and
then the x, as we did with the P i c t u r e type in Chapter I .
We can now define a hierarchy of movable objects; fir\t we have the P o i n t .
270 Algebraic types
d a t a Vector = Vec F l o a t F l o a t
c l a s s Movable a where
move : : Vector -> a -> a
r e f l e c t X : : a -> a
r e f l e c t Y : : a -> a
r o t a t e 1 8 0 : : a -> a
rotate180 = reflectX . reflectY
d a t a Point = Point F l o a t F l o a t
d e r i v i n g Show
To make Point an instance of Movable we have to give definitions of move. r e f lectX
and r e f lectY over the Point type.
/-
Here we can see that the move is achieved by adding the components v l and v2 to
the coordinates of the point. Reflection is given by changing the sign of one of the
coordinates
r e f l e c t X (Point c l c2) = Point c l (-c2)
r e f l e c t Y (Point c l c2) = Point (-cl) c2
For this instance we override the default definition of r o t a t e 1 8 0 by changing the sign
of both coordinates. This is a more efficient way of achieving the same transformation
than the default definition.
r o t a t e 1 8 0 (Point c l c2) = Point ( - c l ) (-c2)
Named objects
Many forms of data contain some sort of name, a S t r i n g which identifies the object in
question. What do we expect to be able to do with a value of such a type?
We should be able to identify the name of a value, and
we ought to be able to give a new name to a value.
These operations are embodied in the Named class:
c l a s s Named a where
lookName : : a -> S t r i n g
giveName : : S t r i n g -> a -> a
L
272 Algebraic types
the one-constructor type whose two components are of type a and S t r i n g . The
i n s t a n c e declaration for this type is
We can then argue that all the operations of the Movable class can be lifted.
i n s t a n c e Movable a => Movable (Name a ) where
move v = mapName (move v)
r e f l e c t X = mapName r e f l e c t X
r e f lectY = mapName r e f l e c t Y
Now we already know that Named (Name a) by (1) above, so if we detine a class
combining these attributes
c l a s s (Movable b , Named b) => NamedMovable b (3)
This last instance is established by showing that the two constraints of (3) hold when
b is replaced by Name a, but this is exactly what (1) and (2) say given the constraint
Movable a.
Algebraic types and type classes 273
d a t a Name a = P a i r a S t r i n g
exam1 = P a i r ( P o i n t 0 . 0 0 . 0 ) "Dweezil"
mapName f ( P a i r o b j nm) = P a i r (f o b j ) nm
14.47 A different way of combining the classes Named and Movable is to establish the
instance
14.48 Show that the method of the previous question can be used to combine instances
of any two classes.
14.49 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, S t r i n g . Show how it can be used to combine an arbitrary instance
of one class with a particular instance of another for any two classes whatever.
14.50 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.51 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.
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
Reasoning about algebraic types 275
map f [I = [I (map. 1)
map f (x:xs) = f x : map f x s (map. 2)
mapTree f N i l = N i l (mapTree . 1)
mapTree f (Node x t l t 2 )
= Node (f x) (mapTree f t i ) (mapTree f t 2 ) (mapTree . 2 )
c o l l a p s e N i l = [I ( c o l l a p s e . 1)
c o l l a p s e (Node x t i t 2 )
= c o l l a p s e t i ++ [x] ++ c o l l a p s e t 2 ( c o l l a p s e . 2)
map f ( c o l l a p s e N i l )
= map f [I by ( c o l l a p s e . 1)
= [I by (map.1)
c o l l a p s e (mapTree f N i l )
= collapse N i l by (mapTree . 1)
= [I by ( c o l l a p s e . 1 )
map f ( c o l l a p s e (Node x t r l t r 2 ) )
= c o l l a p s e (mapTree f (Node x t r l t r 2 ) ) (ind)
map f ( c o l l a p s e (Node x t r l t r 2 ) )
= map f ( c o l l a p s e t r l ++ [XI ++ c o l l a p s e t r 2 ) by ( c o l l a ~ s e . 2 )
= map f ( c o l l a p s e t r l ) ++ [f x] ++ map f ( c o l l a p s e t r 2 )
by (map++)
= c o l l a p s e (mapTree f t r l ) ++ [f XI ++
c o l l a p s e (mapTree f t r 2 ) by (hypl ,hyp2)
The final step is given by the two induction hypothescs, that the result holds for the two
subtrees t r l and t r 2 . The result (map++) is the theorem
c o l l a p s e (mapTree f (Node x t r l t r 2 ) )
= c o l l a p s e (Node (f x) (mapTree f t r l )
(mapTree f t r 2 ) ) by (mapTree .2)
= c o l l a p s e (mapTree f t r l ) ++ [f xl ++
c o l l a p s e (mapTree f t r 2 ) by ( c o l l a p s e .2)
and this finishes the proof in the Node case. As this is the second of the two cases, the
proof is complete.
Proof The proof has two cases. In the first x is replaced by Nothing:
maybe 2 abs Nothing
= 2 2 0
In the second, x is replaced by J u s t y for a defined y.
maybe 2 a b s ( J u s t y)
= abs y > 0
In both cases the result holds, and so the result is valid in general.
with ( a s s o c . 1 ) being the non-primitive recursive case. We would like to prove that
the rearrangement does not affect the value of the expression:
e v a l ( a s s o c ex) = e v a l e x (eval-assoc)
for all finite expressions ex. The induction principle for the Expr type has three cases.
L i t case Prove P ( L i t n).
Add case Prove P(Add e l e2), assuming P ( e l ) and P(e2)
Sub case Prove P(Sub e l e 2 ) , assuming el) and P ( e 2 )
To prove ( e v a l - a s s o c ) for all finite expressions, we have the three cases given above.
The L i t and Sub cases are given, respectively, by ( a s s o c .4) and ( a s s o c .3),but the
Add case is more subtle. For this we will prove
by induction on the number of Adds which are left-nested at the top level of the
expression e l -recall that it was by counting these and noting that a s s o c preserves the
total number of Adds overall that we proved the function would always terminate. Now,
if there are no Adds at the top-level of e l , the equation ( a s s o c . 2 ) gives (eval-Add).
Otherwise we rearrange thus:
e v a l ( a s s o c (Add (Add f l f 2 ) e 2 ) ) )
= e v a l ( a s s o c (Add f l (Add f 2 e 2 ) ) ) by ( a s s o c . 1 )
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.
f Exercises
14.52 Prove that the function weather from Section 14.1 has the same behaviour as
when
14.53 Is it the case that the a r e a of each Shape from Section 14.1 is non-negative'? If
so, give a proof; if not, give an example which shows that it is not the case.
14.54 If we define the s i z e of an NTree thus
size NilT = 0
s i z e (Node x t l t 2 ) = I + s i z e t l + s i z e t 2
s i z e t r < 2l(depth t r )
The next two exercises refer back to the exercises of Section 14.3.
14.56 Prove that the function t w i s t has the property that
twist . twist = id
14.57 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.
j Summary )
Algebraic types sharpen our ability to model types in our programs: we have seen in
this chapter how simple, finite typcs like Temp can be defined, as well as the more
complex E i t h e r 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 polymorphic
higher-order functions can be defined over lists - thi\ carrie\ 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 typey.
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 program development,
this separation of concerns assists the system developer to produce simple and correct
solutions.
Having introduced algebraic data types we are able to give more substantial examples
of classes and their instances. We can see that the overloading that classes bring makes
Reasoning about algebraic types 279
code both easier to read and more amenable to reuse; we can see in particular how
software can he 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 )
Case study: Huffman
codes
15.1 Modules in Haskell
15.2 Modular design
15.3 Coding and decoding
15.4 Implementation - I
15.5 Building Huffman trees
15.6 Design
15.7 Implementation - I I
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
-
Modules in Haskell
As we first saw in Section 2.4, a module consists of a number of definitions (of types,
functions and so on), with a clearly defined interface stating what the module exports
to other modules which use or import it.
Using modules to structure a large program has a number of advantages.
Parts of the system can be built separately from each other. Suppose we want to
monitor traffic on a network: one module might produce the statistics, while another
displays them in a suitable form. If we agree which statistics are to be presented
(their type etc.), that is we agree the interface, then development of the two parts of
the system can go on independently.
Modules in Haskell 281
Parts of a system can be compiled separately; this is a great advantage for a system
of any conlplexity.
Libraries of components can be reused, by importing the appropriate modules con-
taining them.
Module headers
Each module is named, so an example named Ant might be
d a t a Ants = . ..
anteater x = ...
Note that the definitions all begin in the column under the keyword module; it is safest
to make this the leftmost column of the tile, or in the case of a literate script, one tab-stop
in from the leftmost column.
Our convention for file names is that a module Ant resides in the Haskell file Ant. hs
or Ant. l h s .
Importing a module
The basic operation on modules is to import one into another, so in defining Bee we
might say
import Ant
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 a n t e a t e r 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.
282 Case study: Huffrnan codes
Export controls
As we explained when import was introduced, the default is that all top-level definitions
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
reverse : : [a] -> [a]
reverse = shunt [I
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 a n t e a t e r from Ant we can write
stating that we want just the type Ants; we can alternatively say which names we wish
to hide:
import Ant h i d i n g ( a n t e a t e r )
Suppose that in our module we have a definition of bear, and also there is an object
named bear in the module Ant. How can we gain access to both definitions'? The
answer is that we use the qualified name Ant. b e a r 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 n o 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 thus:
import q u a l i f i e d Ant
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
import I n s e c t a s Ant
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 'invisihly', 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,
import q u a l i f i e d Prelude
and use the original words by writing its qualified name Prelude. words.
284 Case study: Huffman codes
Further details
Further information about the Haskell module system can be found in the language
report (Peyton Jones and Hughes 1998); note that some of the details will be different
in particular implementations.
15.1 Can you get the effect of export controls using import'? Can you get the et'fcct of
the qualification9 of import using export controls? Discuss why both directive5
are included in the language.
15.2 Explain why you think il is the default that imported definitions are not thein-
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 syctem?
Any computer system which is used seriously will be modified durin? its lifetime. either
by the person or team who wrote it, or more likely by others. For this reason, all systems
should be designed with C ~ C I M i~n Ymind.
We mentioned this earlier when we said that systems should be documented. with
types given to all top-level definitions, and comments accompanying each script and
substantial definition. Another useful form of description is to link each definition with
proofs which concern it; if we know some o f the logical properties of a function, we
have a more solid conception of its purpose.
Documentation makes a script easier to understand. and therefore change. but we
can give structure to a collection of definitions if they are split among modules or
scripts. each script concerning a separate part of the overall system. The directives
which link the files tell us how the parts of the system fit together. If we want to modify
a particular part of a system, we should therefore be able to modify a single module
(at least initially). rather than starting by modifying the whole of the system as sinsle
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 possible.
Each part of the system should be performed by one module: each module should
do one thing completely; it should be self-contained, in other words. If performing
one part of the whole is split between two modules, then either their code should he
merged, or there should be a module defined with the single purpose of bringing the
two components together.
Each module should export only what is necessary. It is then clearer what the effect of
an import is: precisely the functions which are needed are imported. This process is
often called information hiding in software engineering, which is itself the general
study of principles for prograrnniing in the large.
Modules should be small. As a rule of thumb, no n~oduleshould bc largcr than can
be printed on two or three sides of paper.
We have also mentioned design for reuse, particularly in the context of polymorphic
types and higher-orderfunctions. The module will be the unit ofreuse, and a library will
be accessed hy means of an import statement. Similar principles apply to the design
of libraries. Each library should have a clearly detincd purpose. like implementing a
type together with basic operations over the type. In addition, we can say that
on including a general-purpose module, it is possible to suppress thc delinition\
which are not used:
a qualified import can be used to avoid the name-clashes which can often occur:
despite the (infinite) choice of name5 for functions, in practice we tend to choose
from a very small subset!
The advicc here might seem dry - what has been said is illustratcd 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.
a
Coding and decoding
Electronic messages of various kinds are sent between machines and people by thc
billion each day. Such messages are usually sent as sequences of hinary 'bits'. For the
transmission to be swift, the messages need to be coded as efficiently as possible. The
area we explore here is how to build codes -translalions of characters into sequences
of bits - which produce messages as compact as possible.
Trees can be used to code and decode messages. Consider as an example the tree
We can see this as giving codes for the letters a, b and t by looking at the routes taken
to rcach the letters. For example, to get to b, we go right at the top node, and lqft at the
next:
286 Case study: Huffrnan codes
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 infor~nationabout 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 rncssage RLLRRRRLRR. To
decode we follow the route through the tree given, moving right then left, to give the
letter b,
where we have shown under each tree the sequence of bits remaining to he decoded.
Continuing g a i n from thc top, we have the codes for a then t ,
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.
Implementation - 1 287
( Exercises 1
15.4 What is the coding of the message b a t t a t using the following tree?
Compare the length of the coding with the others given earlier.
15.5 Using the first coding t r w . 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.
(154) Implementation - I
We now begin to implement the Huffman coding and decoding, in a series of Haskell
nlodules. The overall structure of the system we develop is illustrated at the end of the
chapter in Figure 15.4.
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.
t y p e Table = [ (Char,HCode) 1
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.
d a t a Tree = Leaf Char Int I
Node I n t Tree Tree
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 tile; each of the definitions is
prcceded by a comment as to its purpose.
288 Case study: Huffman codes
Types.lhs
Note that we have given a full description of what is exported by the n~odule.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 omitted, but we
include it here as useful documentation of the interface to the module.
To code a message according to a table of codes, we look up each character in the table,
and concatenate the results.
We saw in Section 15.3 that decoding according to the tree tr has two main cases.
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 remainder
of the code at the top of the tree tr.
decodeMessage tr
= decodeByt tr
where
decodeByt (Node n tl t2) (L:rest)
= decodeByt tl rest
decodeByt (Node n ti t2) (R:rest)
= decodeByt t2 rest
decodeByt (Leaf c n) rest
= c : decodeByt tr rest
decodeByt t [I = [I
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
290 Case study: Huffman codes
decodeMessage examlmessl
decodeByt examlmessl
A
.- decodeByt examl[R,L ,L,R,R,R,R,L,R,R]
--+ decodeByt (Node0 (Leaf 'b' 0 ) (Leaf 't' 0 ) )
- [L,L,R,R,R,R,L,R,Rl
decodeByt (Leaf 'b' 0 ) CL,R,R,R,R,L,R,R]
-- ' b' : decodeByt examl [L ,R,R,R,R,L,R,R]
-
-A 'b' : decodeByt (Leaf 'aJ 0 ) [R,R,R,R,L,R,R]
'b' : 'a' : decodeByt examl [R,R,R,R,L,R,R]
Before looking at the implementation any further, we look at how to conslruct 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
The main idea of the translation is to build the tree by taking the two characters
occurring least frequently, and making a sitzgle 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 ' ,I), . . . is turned into a tree, giving the list of trees
Design 291
[ Node 3 (Leaf ' b ' I) (Leaf ' a ' 2) , Leaf ' t ' 3 1
Node 6 (Node 3 (Leaf ' b ' I ) (Leaf ' a ' 2 ) ) (Leaf ' t ' 3)
(15 Design
Implementing the systein will involve us in designing various modules to perform the
stages given above. We start by deciding what the modules will be and the functions 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 information 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. l h s when bringing
the three tiles together. This is given in Figure 15.3.
Our next task is to implement each module in full and we turn to that now.
292 Case study: Huffman codes
Frequency.lhs
MakeCode.lhs
(Is Implementation - II
In this section we discuss in turn the three implementation modules.
Next, we sort the list on the characters, bringing together the counts of equal
characters.
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 perforill 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 x s
I length xs < 2 = xs
I otherwise
= merge (mergesort merge f i r s t )
(mergesort merge second)
where
f i r s t = t a k e half xs
second = drop h a l f x s
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 parutneter that thc
mergesort function becomes reusable.
In sorting the characters, we amalgamate entries for the same character
294 Case study: Huffman codes
freqMerge xs [I = xs
freqMerge [I ys = ys
freqMerge ((p,n):xs) ((q,m):ys)
I (n<m 1 1 (n==m && p<q))
= (p,n) : freqMerge xs ((q,m) :ys)
I otherwise
= (q,m) : freqMerge ((p,n):xs) ys
frequency
= mergesort freqMerge . mergesort alphaMerge . map start
where
start ch = (ch,l)
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 exported.
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
amalgamate : : [ Tree 1 -> [ Tree 1
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'.
armw is marked to indicatc the functions exported by the included module, so that. for
example, codes and codeTable are exported from Makecode. l h s to Main. lhs.
If this coding system were to be used as a component of a larger system, a module
directive could be used to control which of the four functions and the types are expor[cd.
after the module had been renamed. It is important to realize that the types will need
to be exported (or be included in the file including Main. l h s ) if the functions are to bc
used.
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 neccwary, give a version of merge sost which
removes duplicate entries.
15.10 Give a version of lnergc sort which t;~licsan ordering function as a parameter:
which give printable versions of Huffman trees and code tables. One general way
of printing trees is to use indentation to indicate the structure. Schetnatically,
this looks like
( Summary 1
When writing a program of any she, 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 detinitions are exported from one module and
imported into another.
298 Case study: Huffman codes
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
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 Huffrnan coding example. In
particular, it was shown for the file Makecode. lhs and its three sub-modules that
design can begin with the design of modules and their interfaces - that is the detini-
tions (and their types) which are exported. Thus the design process starts h f b r e any
implementation takes place.
Abstract data types
16.1 Type representations
16.2 The Haskell abstract data type mechanism
16.3 Queues
16.4 Design
16.5 Simulation
16.6 Implementing the simulation
16.7 Search trees
16.8 Sets
16.9 Relations and graphs
16.10 Commentary
The Haskell module system allows definitions of functions and other objects to be hidden
when one file is included in another. Those definitions hidden are only of use in defining
the exported functions, and hiding them makes clearer the exact interface between the
two files: only those features of the module which are needed will be visible.
This chapter shows that information hiding is equally applicable for types, giving
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, including queues,
sets, relations and the fundamental types belonging to a simulation case study.
uc-h
n.:ial :: Store
c , le : : S t . L , ~- , '.'.II - . -I I
updlte :: S t c r e > Var -> I n t -> S t o : ~
IMPLEMENTOR
by the Expr type of Section 14.2, but with variables included. The calculator is lo
providz the facility to set the values of variables, as well as for variables Lo form parts
of expressions.
As a par1 of our system, we need to be able to niodel the current values ol'the variables.
which we might call the store of the calculator. How can this be done'? A nuniberof
111odelspresent themselves, including:
Both models allow us to look up and update the values of variables. as well as set a
starting value for the store. These operations have types as follows.
i n i t i a l : : Store
value : : S t o r e -> Var -> I n t (StoreSig)
update : : S t o r e -> Var -> I n t -> S t o r e
but each ~riodelallows more than that: we can, for instance, reverse a list, or coniposea
function with others. In using the lype S t o r e we intend only to use the three operations
givcn, but i t is always possible to use the model i n unintended ways.
How can we give a better model of a store'? The answer is to detine a type which
only has the operations i n i t i a l , value and update. so that we cannot abuse the
representation. Wc 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 ofoperations
we call the type an abstract data type (or ADT). Since the 'concretc' 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
reprcscntation of the type of storcs, there are two other benetits of type abstraction.
The only infornmtion that they have to agree on i\ the signature; once this is agreed.
they can work independently. This is therefore another way of breaking a complex
problem into simpler parts; another aspect of the 'divide and conquer' method.
We can modify the implementation of the S t o r e without having any effect o n thc
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 bc
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.
module S t o r e ( S t o r e , i n i t i a l , v a l u e , u p d a t e ) where
which shows that we can access the type only through the three functions mentioned.
In this book we will adopt the convention that we will also include as comments in
the module header the types of the exported functions, giving in the case of S t o r e the
following header.
module S t o r e
( Store,
initial, -- Store
value, -- S t o r e -> Var -> I n t
update -- S t o r e -> Var -> I n t -> S t o r e
) where
Now, the module must contain a def nition of the S t o r e type and the functions over it.
If the imple~nentationtype was a d a t a type, then this would complete the realization
of the abstract data type. However, in our running example of stores. we suggested
earlier that we would use a list of pairs, [ ( I n t ,Var) 1, to model the type, and so we
will have to define a new d a t a type, called S t o r e
We now have to define the functions i n i t i a l , value and update over the Store
type. One approach is to detine the analogous functions over [ ( I n t ,Var)] and then
to adapt those. We can say
i n s t a n c e Show S t o r e where
show n ( S t o s t o ) = show n s t o
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 S t o r e which are defined
by means ofthese i n s t a n c e declarations will be available whenever the module S t o r e
is imported. Of course, we can choose not to declare these instances, and so not to
provide an equality or a show function over S t o r e s .
Stores as functions
A different implementation of S t o r e is given by the type of functions from variables
to integers.
newtype S t o r e = S t o (Var -> I n t )
i n i t i a l :: Store
i n i t i a l = S t o ( \ v -> 0)
( Exercises 7
16.1 Give an implementation of S t o r e using lists whose entries are ordered according
t o the variable names. Discucs why this might be preferable to the original list
implementation, and also its disadvantages, if any.
16.2 For thc implementation of S t o r e as a list type [ ( I n t , V a r ) l , give a definition
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 implenientation 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 v a l u e of a variable which does not have a value in the given store. Explain
how you would modify both the signature of S t o r e and the two implementations.
16.4 Rather than giving an error when looking up a variable which does not have a
value in the particular 5tore, extend the signature to provide a test of whether a
variable has a value in a given ctore, and explain how you would modify the two
in~plementationsto define the test.
16.5 Suppose you are to implement a fourth operation over S t o r e
s e t A l l : : I n t -> S t o r e
so that s e t A l l 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.
Queues
A queue is a 'first in, first out' structure. If first F l o and then Eddie joins an initially
empty queue, the first person lo 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 ,
empty4 , -- Queue a
isEmptyQ , -- Queue a -> Boo1
Queues 305
addQ 9
-- a -> Queue a -> Queue a
remQ -- Queue a -> ( a , Queue a >
) where
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
newtype Queue a = Qu [a]
remQ
addQ
El remQ remQ
I
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 x s 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 ofqueues.
Initially the queue containing the dements 7, 5, 2 and 3 is shown. 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, where the left-hand list
grows to the left, and the right-hand to the right.
The function remQ removes elements from the head of the left-hand list, and add4
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 (the picture might
be misleading here: remember that the two lists grow in opposite directions).
This case in which we have to transfer elements is expensive, as we have to run along
a list to reverse it, but we would not in general expect to perform this every time we
remove an element from the queue. The I-laskell implementation follows now.
( Exercises ]
16.7 Give calculations of
"abcde" ++ " f "
i n i t "abcdef "
l a s t "abcdef "
where
i n i t x = t a k e ( l e n g t h x-I) x
l a s t x = x ! ! ( l e n g t h x-I)
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 A double-ended queue, or deque, allows elements to be added or removed from
either end of the structure. Give a signature for the ADT Deque a,and give two
different implementations of the deque type.
16.10 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 implementation
of the ADT.
16.1 1 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 implementation of
the ADT of priority queues.
16.12 [Harder] Examine how priority queues could be used to implement the Huffman
coding system in Chapter 15.
Design
This section examines the design of Haskell abstract data types, and how the presence
of this mechanism affects design in general.
308 Abstract data types
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.
First we need to identify and name the types in the system.
Next, we should give an informal description of what is expected from each type.
Using this description we can then move Lo writing the signature of each abstract
data type.
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.
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 ob.jects 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 wc 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 o f 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 following
signature for binary trees can be so classified, for instance.
module Tree
(Tree,
nil, -- Tree a
isNil, -- Tree a -> Bool
isNode , -- Tree a -> Bool
left Sub, -- Tree a -> Tree a
right Sub, -- 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
Simulation 309
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.
size : : Tree a -> Int
size t
I isNil t = 0
I otherwise = 1 + size (leftsub t) + size (rightsub t)
This definition of size is independent of the implementation. and so would not have
to be reimplemented if the implementation type for Tree a changed. This is a good
reason for leaving size out of the signature, and this is a check we can make for any
signature: are all the functions in the signature needed'? We come back to this point,
and the tree type, later in the chapter. Now we look at a larger-scale example.
16.13 Are all the operations in the Tree a signature necessary? Identify those which
can be implemented using the other operations of the signature.
16.14 Design a signature for an abstract type of library databases, as first introduced
in Chapter 5.
16.15 Design a signature for an abstract type of indexes, as examined in Section 10.8.
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 customer
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, t o 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 processing 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.
This description leads directly to a signature declaration
310 Abstract data types
module queuestate
( QueueState ,
addMessage, -- Inmess -> queuestate -> queuestate
queuestep, -- queuestate -> ( Queuestate , [Outmess] )
queuestart, -- queuestate
queueLength, -- Queuestate -> I n t
queuehpty -- queuestate -> Boo1
) 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.
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, -- I n t -> Inmess -> S e r v e r s t a t e -> Serverstate
serverstep, -- S e r v e r s t a t e -> ( S e r v e r s t a t e , [Outmess] )
simulationstep, -- ServerState -> Inmess -> ( ServerState ,
[Outmess] >
serverstart, -- S e r v e r s t a t e
serversize, -- S e r v e r s t a t e -> I n t
shortestqueue -- ServerState -> I n t
) where
In the next section we explore how to implement these two abstract data types. It is im-
portant 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.
Implementing the simulation 31 1
f Exercises
16.16 Are there redundant operations in the signatures of the ADTs QueueStateand
ServerState?
16.17 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.
The queue
In the previous section, we designed the interfaces for the ADT; how d o we proceed with
implementation? First we ought to look again at the description of the QueueState
type. What information does this imply the type should contain'?
There has to be a queue of Inmess to be processed. This can be represented by a list,
and we can take the item at the head of the list as the item currently being processed.
We need to keep a record of the processing time given to the head item, up to the
particular time represented by the state.
In an Outmess, we need to give the waiting time for the particular item being
processed. We know the time of arrival and the time needed for processing - if
we also know the current time, we can calculate the waiting time from these three
numbers.
It therefore seems sensible to define
d a t a QueueState = QS Time S e r v i c e [Inmess]
d e r i v i n g (Eq, Show)
where the first tield 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 meshage, it is put at the end of the list of messages.
addMessage : : Inmess -> QueueState -> q u e u e s t a t e
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 ( t i m e + l ) 0 inRest. In this state the time is advanced by one, processing time
is set to zero and the head item in the list is removed. An output message 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 s e r v [I) = (QS ( t i m e + l ) s e r v [I , [I)
Note that the case of an input message No is not handled here sincc 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 [I
queueLength : : QueueState -> I n t
queueLength (QS - - q ) = l e n g t h q
The server
The server consists of a collection of queues, accessed by integers from 0; we choose
to use a list of queues.
newtype S e r v e r s t a t e = SS [Queuestatel
d e r i v i n g (Eq, Show)
Note that the implementation of this ADT builds on another ADT; this is not unusual.
Now we take the functions in turn.
Implementing the simulation 31 3
Adding an element to a queue uses the function addMessage from the Queuestate
abstract type.
A step of'the server i s given by making a step in each of the constituent queues, and
concatenating together the output mcssages they produce.
serverstep (SS [I )
= (SS [I , [I )
serverstep (SS (q:qs))
= (SS (q':qs') , mess++messes)
where
(4' , mess) = queuestep q
(SS qs' , messes) = serverstep (SS qs)
In making a simulation step, we perform a server step, and then add the incoming
message, if it indicates an arrival, to the shortest queue.
simulationstep
: : ServerState -> Inmess -> ( ServerState , [Outmess] )
simulationstep servSt im
= (addNewObject im servStl , outmess)
where
(servStl , 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 detined using the operations addToQueue
and shortestQueue.
It is in this function that the input messages No are not passed to the queues, as was
mentioned above.
The other three functions of the signature are standard.
314 Abstract data types
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.
serversize : : ServerState -> Int
serversize (SS xs) = length xs
In tinding the shortest queue, we use the queueLength function from the QueueState
type.
shortestQueue : : ServerState -> Int
( Exercises
16.19 If we let
serverstep serverst1
simulationstep (Yes 13 10) serverst1
16.20 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.
Search trees 315
16.21 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.22 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.
module Tree
(Tree,
nil, -- Tree a
isNil, -- Tree a -> Boo1
isNode , -- Tree a -> Boo1
l e f tSub, -- Tree a -> Tree a
316 Abstract data types
Figure 16.3 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 is used.
It is not exported, as it can break the ordered property of search trees i f it is applied to
an arbitrary pair of scarch trees.
Note that the types of insTree,delete,minTree and join contain the context
Ord a. Recall from Chapter 12 that this constraint means that these functions can
only be uscd 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
detinition of search trees it is unsurprising that we use an ordering in these operations.
Now we look at the definitions in Figure 16.3 in turn.
Search trees 31 7
minTree t
I isNil t = Nothing
I isNil tl = Just v
I otherwise = minTree tl
where
tl = leftsub t
v = treeVal t
join tl t2
= Node mini ti newt
where
(Just mini) = minTree t2
newt = delete mini t2
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.
The diagram shows 3 being inserted in the tree
(Node 7 (Node 2 N i l N i l ) (Node 9 N i l N i l ) )
'
Deletion is straightforward when the value is smaller (larger) than the value at theroot
node: the deletion is made in the left (right) sub-trcc. If the value to be deleted lies at
the root, deletion is again simple if either sub-tree is N i l : 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 j o i n two non-Nil trees t l and t2, where it is assumed that t l is smaller than
t 2 , we pick the minimum element, mini, of t2 to be the value at the root. The left
sub-tree is t i , and the right is given by deleting mini from t2. The picture shows the
deletion of 7 ti-on1
(Node 7 (Node 2 N i l N i l ) (Node 9 (Node 8 N i l N i l ) N i l ) )
The minTree function returns a value of type Maybe a, since a N i l tree has no
minimum. The Just constructor therefore has to be removed in the where clause
of j o i n .
indexT n t (indexT)
1 isNil t = e r r o r "indexTM
1 n < st1 = indexT n t l
I n == s t 1 = v
I otherwise = indexT (n-stl-1) t 2
where
v = treeVal t
tl = leftsub t
t 2 = rightsub t
st1 = size tl
s i z e : : Tree a -> I n t
size t
I isNil t = 0
I otherwise = 1 + s i z e ( l e f t s u b t ) + s i z e (rightsub t )
If we are often asked to index elements of a tree, wc will repeatedly have to tind the
s i z e of search trees, and this will require computation.
We can think of making the size operation Inore efficient by chunxin,y the imple-
mentation of Tree a, so that an extra field is given in an S t r e e to hold the size of the
tree:
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 N i l = 0
s i z e (Node - n - -) = n
Exercises )
16.25 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.26 Define the functions
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.27 Redefinc the functions of the Tree a signature over the Stree implementation
tY Pe.
16.28 To speed up the calculation of maxTree and other functions, you could imagine
storing the maximum and minimum of the sub-tree at each node. Redefine
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.29 You are asked to implement search trees with a count of the number of times an
element occurs. How would this affect the signatureof the type'? How would you
implement the operations'? How much of the previously written implementation
could be re-used?
16.30 Using a modified vcrsion of search trees instead of lists, reimplement the indexing
software of Section 10.8.
Sets 321
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.
(168) 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
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 singlc
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 o f the operations at the same time as giving their
implementation.
module Set
( Set ,
empty , -- Set a
sing , -- a -> Set a
memSet , -- Ord a => Set a -> a -> Boo1
union,inter,diff , -- Ord a => Set a -> Set a -> Set a
322 Abstract data types
) where
There are numerous possible signatures for sets, some of which assume certain proper-
ties 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 S e t 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 S e t a are given in Figures 16.4 and 16.5. At the startof
the file we see that we import the library L i s t , but as there is a definition of union in
there we have to hide this on import, thus,
import L i s t h i d i n g ( union )
Also at the start of the file we give the i n s t a n c e declarations for the type. It is important
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.4 and 16.5. In our descriptions we use curly brackets '{', ']', to represent sets in
examples - this is emphatically not part of Haskell notation.
The empty set (1 is represented by an empty list. and the singleton set {x],consisting
of the single element x, by a one-element list.
To test for membership of a set, we define memset. It is important to see that we
exploit the ordering in giving this definition. Consider the three cases where the list is
non-empty. In (memSet . I ) , the head element of the set, x,is smaller than the element
y which we seek, and so we should check recursively for the presence of y in the tail
xs. In case (mernset. 2) we have found the element, while in case (memSet .3)the
head element is larger than y; since the list is ordered, all elements 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.
The functions union, i n t e r , d i f f give the union, intersection and difference of two
sets. The union consists of the elements occurring in either set (or both), the intersection
of those elements in both sets and the difference of those elements in the first but not the
second set we leave the definition o f d i f f as an exercise for the reader. For example,
-
Sets 323
import L i s t h i d i n g ( union )
i n s t a n c e Eq a => Eq ( S e t a) where
(==) = eqSet
i n s t a n c e Ord a => Ord ( S e t a) where
(<=) = l e q S e t
newtype S e t a = S e t I [a]
empty : : S e t a
empty = S e t I [I
s i n g : : a -> S e t a
s i n g x = S e t I [x]
Figure 16.4 Operations over the set abstract data type, part 1 .
324 Abstract data types
card : : S e t a -> I n t
card (SetI xs) = length xs
Figure 16.5 Operations over the set abstract data type, part 2.
Sets 325
which works directly over ordered lists, and then make a version which works over S e t ,
union : : Ord a => S e t a -> S e t a -> S e t a
union ( S e t I x s ) ( S e t I ys) = S e t I ( u n i xs ys)
Recall that the brackets '(', '1' are not a part of Haskell; we can see them as shorthand
ibr Haskell expressions as follows.
{el, ... , e n } = makeset [ e l , - .. ,en]
To test whether the first argument is a subset of the second, we use s u b s e t ; 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 cquality on S e t 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
i n s t a n c e Eq a => Eq (Set a ) where
(==) = eqSet
The ADTequality will not in general be the equality on the underlying type: if we were
to choose arbitrary lists to niodel sets, the equality test would be more complcx, since
[ I , 23 and [2,1,2,21 would represent the same set.
We also export list ordering as an ordering over S e t .
i n s t a n c e Ord a => Ord (Set a ) where
(<=I = leqSet
Thc subset ordering is not bound to <= since it is customary for thc <= in Ord to be a
total order, that is for all elemcnts 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.
To form a set from an arbitrary list, makeset, the list is sorted, and then duplicate
elements are removed, before it is wrapped with S e t I . The definition of s o r t is
imported from the L i s t library.
mapset, f i l t e r S e t and f o l d s e t behave like map, f i l t e r and f o l d r except that
they operate over sets. The latter two are essentially given by f i l t e r and f o l d r ; in
mapset duplicates have to be removed after mapping.
showset f ( S e t I xs) gives a printable version of a set, one item per line, using
the function f to give a printable version of each element.
326 Abstract data types
The cardinality of a set is the number of its members. The function c a r d gives this, as
it returns the l e n g t h of the list.
In the next section we build a library of functions to work with relations and graphs
which uses the S e t library as its basis.
( Exercises
16.32 Compare how the following pairs of sets are related by the orderings <= and
subset.
which gives the symmetric difference of two sets. This consists of the elements
which lie in one of the sets but not the other, so that
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'?
16.36 How are the functions
which return the union and intersection of a set of sets defined using the opera-
tions of the abstract data type'?
16.37 Can infinite sets (of numbers, for instance) be adequately represented by ordered
lists? Can you tell if two infinite lists are equal, for instance'?
Relations and graphs 327
16.38 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.39 Give an implementation of the Set abstract data type using search trees.
16.40 Give an implementation of the search tree abstract data type using ordered lists.
Compare the behaviour of the two implementations.
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
In general we say
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 relations like
isparent and issibling gives the relationship of being either a parent or a sibling.
and so on.
We look at two particular 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.6.
1. Working bottom-up, we first ask how we find all elements related to agivcn 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 function to perform this is image and the set
of Ben's children will be
image isparent Ben = {Sue,Leo]
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 ot
sets is given by folding the binary union operation into the set.
328 Abstract data types
setproduct : : (Ord a,Ord b) => Set a -> Set b -> Set (a,b)
setproduct st1 st2 = unionset (mapset (adjoin stl) st21
2. The second task we set ourselves was to find the isAncestor relation. The
general problem is to find the transitive closure of a relation, the function tClosure
of Figure 16.6. We do this by closing up the relation, so we add grandparenthood.
great-grandparenthood and so forth to the rclation until nothing further is added. We
explain transitive closure formally later in t h i 4 section.
How do wc get the isGrandparent relation? We match together pairs like
and see that this gives that Ben is a grandparent of Joe. We call this thc relational
composition of isparent with itself. In general,
isGrandparent
= isparent 'compose' isparent
= ((Ben,Joe)]
In defining compose we have used the setproduct fimction to give the product of
two scts. This is formed by pairing every element of the first set with every element of
the second. For instance,
setproduct (Ben,Suzie] {Sue,Joe]
= { (Ben,Sue) , (Ben,Joe) , (Suzie,Sue) , (Suzie,Joe) )
setproduct uses the function adjoin to pair each element of a set with a given element.
For instance,
adjoin Joe {Ben,Sue) = { (Ben,Joe) , (Sue,Joe) }
A relation re1 is transitive if for all (a,b) and (b,c) in rel, (a,c) is in rel. The
transitive closure of a rclation re1 is the smallest relation extending re1 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-ordcr filnction
of general use. limit f x gives the limit of the sequence
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.
As an example, take Ben to be Sue's father, Sue to be Joe's mother, who himsclf has
no children. Now define
addchildren : : Set Person -> Set Person
330 Abstract data types
to add to a set the children of all members of the set, so that for instance
addchildren {Joe,Ben) = {Joe,Sue,Ben]
Now we can give an example calculation of a limit of a function over sets.
limit addchildren {Ben)
Context simplification
The functions of Figure 16.6 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
instance (Ord a, Ord b) => Ord (a,b) .... (pair)
and so this is sufficient to ensure Ord (a,b), which is required for the application of
mapset within adjoin.
Similarly. in defining compose we require an ordering on the type ( (a,a) ,(a,a));
again, knowing Ord a is sufficient to give this, since (pair) can be used to derive the
ordering on ((a,a) ,(a,a) 1.
Graphs
Another way of seeing a relation is as a directed graph. For example, the relation
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
graphl 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 1to 3 then to 2 and finally to 4, while the pair
(2,l) is not in the closure, since no path leads from 2 to I through graphl.
Relations and graphs 331
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 graphl? We start with the
set
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 re1 = mapset (addImage re11
332 Abstract data types
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-tirst and breadth-first. In a depth-first
search, we explore all elements below a given child before moving to the nexr child: a
breadth-first search examines all the children before examining the grandchildren, and
so on. In the case of searching below node 1 in graphl, the sequence [ I , 2,4,31 is
depth-first (4 is visited before 3), while [ I , 2,3,41 is breadth-first. These examples
show that we can characterize the searches as transformations
which returns the set of descendants of v in r e 1 which are not in the set s t . 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 S e t abstract data type a function
f l a t t e n : : S e t a -> [a1
f l a t t e n (Set1 xs) = xs
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 particular
definition, we assume the existence of a flatten function of type ( s e t L i s t ) . Then we
can say
Breadth-first search
A breadth-first search involves repeatedly applying f indDescs until a h i i t is reached.
The l i m i t function discussed earlier will find this, so we define
Relations and graphs 333
Depth-first search
How does depth-first search proceed'? We first generali~ethe problem to
Here we call the auxiliary function depthlist,which finds all the descendants of a
list of nodes.
depthList : : Ord a => Relation a -> [a] -> [a] -> [a]
next gives the part of the graph accessible below v a l . This may be [I, if val is a
member of the list used, otherwise depthsearch is called.
d e p t h L i s t is then called on the tail of the list, but with next appended to the list of
nodes already visited.
16.41 Calculate
c l a s s e s (connect graph11
c l a s s e s (connect graph21
which gives the length of a shortest path from one node to another in a graph.
For instance,
distance graphl 1 4 = 2
d i s t a n c e graphl 4 1 = 0
0 is the result when no such path exists, or when the two nodes are equal.
16.44 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.
(1610! Commentary
This section explores a number o f issues raised by the introduction o f ADTs into our
repertoire.
First, we have not yet said anything about verification o f functions over abstract data
types. This is because there is nothing new to say about the proof o f 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 constraints 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
since this breaks the information-hiding barrier and reveals something o f the imple-
mentation itself.
Next we note that our implementation o f sets gives rise to some properties which
we ought to prove, often called proof obligations. We have assumed that our sets are
implemented as ordered lists without repetitions; we ought to prove that each operation
over our implementation preserves this property.
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 o f different types.
The signature in an ADT is associated with a single implementation type, which may
be rnonornorphic or polymorphic. On the other hand, the signature in a class will
be associated with multiple instances; this is the whole point o f including classes, in
fact.
The functions in the signature of an ADT provide the only access to the underlying
type. There i s no such information hiding over classes: to be a member o f a class, a
type must provide at least the types in signature.
ADTs can be polymorphic, so we can have a polymorphic type o f search trees, for
instance. Classes classify single types rather than polymorphic families o f types;
constructor classes as discussed in Chapter 18 extend classes to do exactly that.
( Summary )
The abstract data types o f this chapter have three important and related properties.
They provide a natural representation o f a type, which avoids being over-specific.
An abstract data type carries precisely the operations which are naturally associated
with the type and nothing more.
336 Abstract data types
The signature of an abstract data type is a firm interface between the user and the
iniplernenter: development of a system can proceed completely independently on
the two sides of the interface.
If the implementation of a type is to be modified, then only the operations in the
signature need to be changed; any operation using the signature functions can be used
unchanged. Wc saw an example of this with search trees, when the implementation
was modified to include size information.
We saw various examples of ADT development. Most importantly we saw the practical
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 difticulties 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 detinitions.
( Chapter I7
Lazy programming
17.1 Lazy evaluation
17.2 Calculation rules and lazy evaluation
17.3 List comprehensions revisited
17.4 Data-directed programming
17.5 Case study: parsing expressions
17.6 Infinite lists
17.7 Why infinite lists?
17.8 Case study: simulation
17.9 Proof revisited
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 i s 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 will be examined.
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 processors of all
sorts. One of the distinctive features of a functional language is the collection of facilities
it provides for defining such sets of building blocks.
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 processing programs
338 Lazy programming
- 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 de.
scribe 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 random
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.
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 1 7.1 and 17.2 are essential reading, but it is possible to follow as much of the
remainder as you like: the chapters which follow do not depend upon it.
Lazy evaluation
Central to evaluation i n Haskell is function application. The basic idea behind this is
simple; to evaluate the function f applied to arguments al, a2, . . . , ak, we simply
substitute the expressions ai for the corresponding variables in the definition of the
function. For instance, if
then
I n this example, both o f the arguments are evaluated eventually, but this is not always
the case. If we define
then
Lazy evaluation 339
Here (9-3) is substituted tbr 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
switch : : I n t -> a -> a -> a
switch n x y
I n>O = x
I otherwise = y
If the integer n is positive, the result is the value of x; otherwise it is the value 01' 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
so that
(h-eval)
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
i. ii.
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 informal
introduction to lazy evaluation, which can be summarized in the three points:
arguments to functions are evaluated only when this is necessary for evaluation to
continue;
an argument is not necessarily evaluated fully: only the parts that arc needed are
examined;
an argument is evaluated at most only once. This is done in the implementation by
replacing expressions by graphs and calculating over them.
We now give a more formal account of the calculation rules which embody lazy
evaluation.
f PI P2 . . . Pk
I gi = el
I g2 = e2
...
I otherwise = er
where
v1 al,l ... = rl
... .
f ql 92 . .. qk
-
- *..
In calculating f a1 ... ak there are three aspects.
evaluation proceeds using the first equation; if not, they are checked against the second
equation, which may require further evaluation. This is repeated until a match is given,
or until there are no more equations (which would generate a Program error). For
instance, given the definition
f : : [ I n t ] -> [ I n t ] -> I n t
f [I ys = 0
f (x:xs) [I = 0
f (x:xs) (y:ys) = x+y
At stage (I), there is not enough information about the arguments to determine whether
there is a match with (f . 1). One step of evaluation gives (21,and shows there is not
a match with (f . I).
The first argument of (2) matches the first pattern of (f .21, 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 explanation).
The expressions a1 to ak are substituted for the patterns pl 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
then
--
f (2+3) (4-1) (3+9)
?? (2+3)>=(4-1) && (2+3)>=(3+9)
?? 5>=3 && 5>=(3+9)
??
?? -True && 5>=(3+9)
5>=(3+9)
??
?? -5>=12
False
?? 3>=5 && 3>=12
Lazy programming
??
??
-- False && 3>=12
False
- ??
12
otherwise ?-i True
f m n
1 notNil xs = front xs
1 otherwise = n
where
xs = [m . . n]
notNil [] = False
notNil (-:-I = True
f 3 5
?? notNil xs
??
?? 1where
xs = C3 . 51 .
??
?? -
?? I. True
?-i 3:[4 .. 51
notNi1 (3:[4 . . 51 )
-+ front xs
where
I. 3+4
-
xs = 3: [4 . . 51
3:4: [5]
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 (31,and so the result.
Calculation rules and lazy evaluation 343
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
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. Plus needs both its
arguments to return a result, but the equality on lists can return False on comparing
[I and (x:xs) without evaluating x or xs. In general the language is implemented so
that no manifestly unnecessary evaluation takes place.
Recall that i f . . . t h e n . . . e l s e . . . ; cases; l e t and lambda expressions can be used
in forming expressions. Their evaluation follows the form we have seen for function
applications. Specifically, i f . . . t h e n . . . e l s e . . . is evaluated like aguard. cases like
a pattern match, l e t like a where clause and a lambda expression like the application
of a named fi~nctionsuch 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 twice, is the order in which applications are evaluated when there is a choice.
where one application encloses another, as seen in the expression. the outer one.
f 1 e l (f e2 171, is chosen for evaluation.
Otherwise, cvuluation is from left to right. In the expression
the underlined expressions are both to be evaluated. The left-hand one, f 1 e l , will
be examined first.
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
programming.
344 Lazy programming
Syntax
A list comprehension has the form
C e 1 q1 , . - . , gk 1
where each qualifier q i has one of two forms.
It can be a generator, p <- lExp, where p is a pattern and lExp is an exprcs4on of
list type.
It can be a test, bExp, which is a boolean expression.
An expression lExp or bExp appearing in qualifier q i can refer to the variables used
in the patterns of qualitiers q l 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 chosen.
p a i r s [1,2,3] [4,51
-- C(1,4),(1,5),(2,4>,(2,5),(3,4),(3,5)1
The first element of xs, 1, is given to x, and then for thisjixed value all possible values
of y in y s 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
t r i a n g l e : : I n t -> [ ( I n t , I n t ) ]
t r i a n g l e n = C (x,y) I x <- C1 .. n] , y <- [I .. x] 1
the second generator, y <- [I .. x] depends on the value of x given by the tirst
generator.
List comprehensions revisited 345
For the first choice of x, 1, the value of y is chosen from [I . . I ] , for the second
choice of x, the value of y is chosen from [I . . 21, and so on.
Three positive integers form a Pythagorean triple if the sum of squares of the lirst
two is equal to the square of the third. The list of all triples with all sides below n
particular bound, n, is given by
since 2 matches x, and [3,41 matches xs when (2, [3,4] ) is matched against (x ,xs) .
We now explain list comprehensions. The notation looks a bit daunting, but the effect
should be clear. The generator v <- [a1 , . . . ,an] has the effect of setting v to the
values a 1 to an in turn. Setting the value appears in the calculation as substitution of
a value for a variable.
where the values 1 and 2 are substituted for x. The rules for tests are simple,
Ce I True , q2 , . . . , q k l
--i [ e I q 2 , . . - , ~ 1
-[ e I F a l s e , q2 ,
[I
.. . qk 1
-
-- [
[
2+y l
2+2 1
y <- [ 2 , 3 , 4 1 I
] ++ [ 2+3 I I ++ [ 2+4 1 1
triangle 3
-- [ (x,y> l x <- [I ..
31 , y <- .-
-- [ ( 1 , ~ )I y<- [l . . 11 1 ++
[ ( 2 , ~ )I y <- 11 . . 21 I ++
[ ( 3 , ~ )I y <- C1 . . 31 I
-- C ( 1 , 1 ) I I ++
[ (2,l) 1 I ++ [ (2,2) I I ++
We now look at two longer examples, the solutions for which are aided by the list
conlprehension style.
List comprehensions revisited 347
( Example 1
List permutations
A permutation of a list is a list with the same elements in a different order. The perms
function returns a list of all permutations of a list.
perms : : Eq a => [a] -> [[a] 1
The empty list has one permutation, itself. If xs is not empty, a permutation is given
by picking an element x from xs and putting x at the front of a permutation of the
remainder xs\\ Cxl . (The operation ' \ \ ' returns the difference of two lists: xs\\ys is
the list xs with each element of ys removed, if it is present.) The definition is therefore
perms [I = CCl I
perms xs = C x:ps I x <- xs , ps <- perms (xs\\Cxl) I
Example evaluations give, for a one-element list,
-
perms C21
[x:ps l x <- C21 , ps <- perms [I 1
[x:psI x < - C21 , ps <- C[11 1
--
* [2:psl ps <- [Cll I
[2:[1 1 1
[[211
for a two-element list,
perms C2,31
-
--. [ x:ps I x <- C2,31 , ps <- perms ( [2,3l\\ Cxl) 1
[ 2:ps I ps <- perms [31 ] ++ [ 3:ps I ps <- perms C21 I
-
-- [ 2:[31 I ++ C 3:[21 I
C2,31 , [3,21 I
and finally for a three-elenlent list,
perms C1,2,3]
[ x:ps I x <- C1,2,31 , ps <- perms([l,2,3]\\[xl) I
2-. [ 1:ps I ps <- perms C2,311 ++...++ C 3:ps I ps <- perms [1,2]]
-- C 1:ps I ps<-CC2,31,c3,2111 ++. . .++ C 3:ps I ps<-CC1,21,C2,1111
-- ~~1,2,3~,~1,3,21,~2,1,31,[2,3,11,~3,1,21,~3,2,111
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 soniewhcre. The
possible insertion points are given by finding all the possible splits of the list into two
halves.
perm : : [a] -> C [a] I
perm [I = [[I1
perm (x:xs) = [ ps++[xl ++qs I rs <- perm xs ,
(ps,qs) <- splits rs 1
348 Lazy programming
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 givcn by a split of
YS.
Before moving on, observc 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.
-
mu1 [2.0,3.1] [4.1,5.0]
sum [8.2,10.0,12.71,15.51
u 46.41
since all combinations of pairs from the lists are taken. In order to multiply together
corresponding pairs, we first zip the lists together:
scalarProduct : : Vector -> Vector -> Float
scalarProduct xs ys = sum [ x*y I (x,y) <- zip xs ys 1
and a calculation shows that this gives the required result. (It is also possible to use
zipwith to detine scalarProduct.) A matrix like
can be thought of as a list of rows or a list of columns; we choose a list of rows here.
List comprehensions revisited 349
Two matrices M and P are multiplied by taking the scalar products of rows of M with
columns of P.
We therefore define
matrixProduct : : Matrix -> Matrix -> Matrix
matrixproduct m p
= 1 CscalarProduct r c I c <- columns p] I r <- m 1
where the function columns gives the representation of a matrix as a list of columns.
columns : : Matrix -> Matrix
The expression [ z ! ! j I z <- y 1 picks the jth element from cach row z in y: [hi\
is exactly the jth column of y. length (head y) is the length of a row in y. and so
the indices j will bc in the range 0 to s = length (head y)-1. Another variant of
the columns function is transpose which is in the library List .hs.
( Exercises \r
17.1 Give a calculation of the expression
350 Lazy programming
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,41 and [3,41 are sublists of [2,3,41, but
only [3,4] is a subsequence.
17.3 Give calculations of the expressions
perm C21
perm [2,31
perm [1,2,31
and of the matrix multiplication
17.7 Give the precise rules for calculating with a generator containing a refutable
pattern, like (x:xs) <- 1Exp. You might need to define auxiliary functions to
do this.
17.8 List coniprehensions can be translated into expressions involving map, f i l t e r
and concat by the following equations.
[ x I x<-xs I = xs
[ f x l x<-xs 1 = map f x s
C e 1 x<-xs , p x , . . . 1 = [ e 1 x < - f i l t e r p xs , . . . I
[ e I x<-xs , y<-ys , . . 1 = concat [ [ely<-ys, . .I 1 x<-xsl
-
2-t sum (map ( - 4 ) ( 1 : [ 2 . . n l ) )
sum ( ( - 4 ) I : map (-4) [2 . . n])
-
-- (1-4) + sum (map (-4) [2 . . n])
1 + sum (map (-4) [2 . . n])
-
'-4 ...
1 + (16 + sum (map (-4) [3 .. n]))
-
^v) ...
1 + (16 + (81 + ... + n4))
As can be seen, none of the intermediate lists is created in this calculation. As soon
as the head of the list is crcated, its fourth power is taken, and it becomes n part of the
sum which produces the final result.
Examples
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,51
i n s 8 ( i n s 6 ( i n s 1 ( i n s 7 ( i n s 5 [I))
--
2-t i n s 8 ( i n s 6 ( i n s 1 ( i n s 7 C51)))
i n s 8 ( i n s 6 ( i n s 1 (5 : i n s ? [ I ) ) )
i n s 8 ( i n s 6 ( 1 : (5 : i n s 7 11 1))
2-t i n s 8 ( 1 : i n s 6 ( 5 : i n s 7 [I))
1 : i n s 8 ( i n s 6 ( 5 : i n s 7 [I ) )
352 Lazy programming
As can be seen from the underlined parts of the calculation, each application of ins
calculates the minimum of a largcr part of the list, since the head of the result of insis
given in a single step. The head of the whole list is determined in this case without us
working out thc value of the tail, and this means that we have a sensible algorithm for
minimum given by (head . iSort).
routes' 1 4
2-, [ 1:r 1 z <- nbhrs' 1 , r <- routes' z 4 ]
-- [ l:r I z <- [2,3] , r <- routes' z 4 1
-- [ 1:r I r <- routes' 2 4 ] ++
- [ 1:r I r
[ l:r I r
-- [ 1:r 1 r
<-
<-
<-
routes' 3 4 1
[ 2:s I w <- nbhrs' 2 , s <- routes' w 4 ]I++ ...
[ 2 : s I w <- [41 , s <- routes' w 4 1 I ++ . . .
(1)
routes1 1 6 = . . .
--t [ l:r 1 r <- routes' 2 6 ] ++
[ 1:r 1 r <- routes7 3 6 1
-
-W ...
[ 1:r
[ 1:r
I r <- [ 2:s I s <- routes' 4 6 1 I ++
I r <- routes' 3 6 1 ($1
Corresponding points in the calculations are marked by (t) and ($1. The search for
routes from 4 to 6 will fail, though, as 4 has no neighbours - we therefore have
The effect of this algorithm is to backtrack when a search has failed: there is no route
from 1 to 6 via 2, so the other possibility of going through 3 is explored. This is done
only when the first possibility is exhausted, however, so lazy evaluation ensures that
this search through 'all' the paths turns out to be an efficient method of finding a single
path.
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 arbitrary graph. We add
a list argument for the points not to be visited (again), and so have
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 re1 is given by routesC re1 x y [I.
routes graphEx 1 4
routes re1 x y
1 x==y = CCxl1
I otherwise = [ x:r I z <- nbhrs r e l x ,
r <- routes re1 z y ,
not (elem x r) I
and explain why this definition is not suitable for use on cyclic graphs. Finally,
give a calculation of
We have already seen the definition of Expr, the type of arithmetic expressions. in
Section 14.2 and in a revised version given on page 255:
data Expr = Lit Int I Var Var I Op Ops Expr Expr
data Ops = Add I Sub I Mu1 I Div I Mod
and showed there how we could calculate the results of these cxpressions using the
function eval. Chapter 16 began with a discussion of how to represent the values
held in the variables using the abstract data type Store. Using these components. we
can build a calculator for simple arithmetical expressions, but the input is unacceptably
crude, as we have to enter members of the Expr type, so that to add 2 and 3, we are
forced to type
Op Add (Lit 2) (Lit 3) (exp)
Case study: parsing expressions 355
What we need to make the input reasonable is a function which performs the reverse of
show: i t will take the text l1 (2+3) " and return the expression (exp).
Constructing a parser for a type like Expr gives a r e a d function which essentially
gives the functionality of the Read class, introduced in Section 12.4 above. Note,
however, that the d e r i v e d definition of r e a d for Expr will parse strings of the form
"Op Add ( L i t 2) ( L i t 3) " rather than the more compact form which we read with
our parser.
Suppose that b r a c k e t and number are the parsers of this type which recognize brackets
and numbers then we have
The problem evident here is that a parser can return more than one result - as in number
"234" - or none at all, as seen in the final case. Instead of the original type, we suggest
Each element in the output list represents a successful parse. In number "234" we
see three successful parses, each recognizing a number. In the first, the number 2 is
recognized, leaving "34" unexamined, for instance.
The type Reads b, which appears in the standard prelude and is used in defining
the Read class, is a special case of the type Parse a b in which [a] is replaced by
S t r i n g , that is, a is replaced by Char.
These parsers allow us to recognize single characters like a left bracket, or a single
digit,
b r a c k e t = token ' ( '
dig = spot isDigit
i n f i x r 5 >*>
none : : P a r s e a b
none i n p = [I
succeed : : b -> P a r s e a b
succeed v a l i n p = [ ( v a l , i n p ) ]
a l t : : P a r s e a b -> P a r s e a b -> P a r s e a b
a l t p l p2 i n p = p l i n p ++ p2 i n p
l i s t : : P a r s e a b -> P a r s e a [bl
list p = (succeed [ I ) ' a l t '
( ( P >*> l i s t p ) ' b u i l d ' (uncurry (:))I
Combining parsers
Here we build a library of higher-order polymorphic functions, which we then use lo
give our parser for expressions. First we have to think about the ways in which parsers
need to be combined.
Looking at the expression cxamplc. an cxprcsqion 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 a l t
a l t : : P a r s e a b -> P a r s e a b -> P a r s e a b
358 Lazy programming
a l t p l p2 i n p = p l i n p ++ p2 i n p
The parser combines the results of the parses given by parsers p l and p2 into a single
list, so a success in either is a success of the combination. For example,
i n f i x r 5 >*>
(>*>) p l p2 i n p
= [ ( ( y , z ) ,rem2) I (y,reml) <- p l i n p , (z,rem2) <- p2 reml 1
The values (y,reml) run through the possible results of parsing i n p using pl. Foi
each of these, we apply p2 to reml, which is the input which is unconsumed by p l ir
that particular case. The results of the two successful parses, y and z, are returned asr
pair.
As an example, assume that number recognizes non-empty sequences of digits, ant
look at (number >*> b r a c k e t ) "24(". Applying number to the string "24(" give!
two results,
-
and
This shows we have one successful parse, in which we have recognized the number 24
followed by the left bracket ' ( ' .
Om final operation is to change the item returned by a parser, or to build somelhing
from it. Consider the case of a parser, diglist,which returns a list of digits. Can we
make it return the number which the list of digits represents? We apply conversion to
the results, thus
build : : Parse a b -> (b -> c) -> Parse a c
( Exercises 1
17.10 Define the functions
so that neList p recognizes a non-empty list of the objects which are recognized
by p,and optional p recognizes such an object optionally - it may recognize
an object or succeed immediately.
360 Lazy programming
(Here the constructor V a r is used as a function taking a character to the type Expr.)
An operator expression will consist of two expressions joined by an operator, the
whole construct between a matching pair of parentheses:
opExpParse
= (token ' ( ' >*>
parser >*>
s p o t isOp >*>
parser >*>
token '1')
' b u i l d ' makeExpr
Exercises >
17.12 Detine the functions
so that
charlistToExpr "234" 1
.. Lit 234
charlistToExpr ""98" -..t Lit (-98)
var : expr
17.18 (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
Expr ::=Int I Var I (Expr Ops Expr) 1
Lexpr Mop Mexpr I Mexpr Aop Expr
Lexpr : : = I n t I Var I (Expr Ops Expr)
Mexpr : := I n t I Var 1 (Expr Ops Expr) I Lexpr Mop Mexpr
Mop ::= '*' 1 '/J 1 '1'
AOP : := J + J I J - J
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
The type Parse a b is represented by a function type, so that all the parser combi-
nators are higher order functions.
Because of polymorphism, we do not need to be specific about either the input or the
output type of the parsers we build.
In our example we have confined ourselves to inputs which are strings of 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 generated
on drrncrntl 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 using
a modern functional progra~nminglanguage 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 iniplementation
open to extension by users, which is the way in which other Haskell libraries are made
available.
17.19 Detine a parser which recognizes strings representing Haskell lists of integers.
like " C2,-3,451".
17.20 Define a parser to recogniye 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 recognise a string. You will also need to define a function
364 Lazy programming
whose parameter is afunction which tests elements of the input typc, and return!
the longest initial part of the input, all of whose elements have the requirer
property. For instance
-
addFirstTwo ones
rcri
addFirstTwo (1:ones)
addFirstTwo - ( I :1 :ones)
?n 1+1
- 2
Infinite lists 365
These functions are also defined over any instance of Enum; details can be found i n
Prelude. hs.
List comprehensions can also define infinite lists. The list of [ill Pythagorean triples
is given by selecting z in [2 . . 1, and then selecting suitable values o f x and y below
that.
powers : : I n t -> [ I n t ]
powers n = [ n-x I x <- CO . . 1 I
and this is a spccial case of the prelude function i t e r a t e , which gives the infinite list
( Examples )
cancelling out all the multiples of numbers, once they are established as prime. Th
primes are the only elements which remain in the list. The process is illustrated i
Figure 17.2.
We begin with the list of numbers starting at 2. The head is 2, and we remove all th
multiples of 2 from the list. The head of the remainder of the list, 3, is prime, sinc
it was not removed in the sieve by 2. We therefore sieve the remainder of the list (
multiples of 3,and repeat the process indefinitely. As a Haskell definition, we write
primes : : CIntl
primes = sieve [2 . . I
sieve (x:xs) = x : sieve [ y I y <- xs , y 'mod' x > 01
primes
-
?-. sieve [2 . . I
2 : sieve [ y l y < - [3 .. y 'mod' 2 > 01
- 2 : sieve ( 3 : [ y I y <-
2 : 3 : sieve [ z I z <-
z 'mod'
. ] , y 'mod' 2 > 01)
y <- [4 . . 1 , y 'mod'2>0],
01
...
-
^vt
I x<n = memberord xs n
I x==n = True
I 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,. . . I 6
--t
memberord [3,5,7,11,. . .I 6
memberOrd [5,7,ll,...1 6
ny) memberOrd [7,ll, . . . I 6
"..i False
seed = 17489
multiplier = 25173
increment = 13849
modulus = 65536
The numbers in this sequence, which range from 0 to 65535, all occur with the same
frequency. What are we to d o 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:
368 Lazy programming
pythagTriples2 =
= C (x,y,z> I x < - C2 . . I ,
y <- [x+l . . 1 ,
z <- [y+l .. 1 ,
x*x + y*y == z*z I
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:
This is left as an exercise. Using such a function it is possible to adapt the definition
of pythagTriples2 to make it give all the Pythagorean triples.
\
/ Exercises
factorial = [l,I,2,6,24,l20,720,. . .1
fibonacci = [O,l,l,2,3,5,8,l3,2l, . . . 1
which returns a list containing the factors of a positive integer. For instance,
370 Lazy programming
factors 12 = [1,2,3,4,6,12]
Using this function or otherwise, define the list of numbers whose only prin
factors are 2,3 and 5,the so-called Hamming numbers:
hamming = ...
[l,2,3,4,5,6,8,9,lO,l2,l5,
of a list
17.25 Define the function inf initeproduct specified above, and use it to correct11
definition of pythagTriples2.
Haskell supports infinite lists and other infinite structures, and we saw in the last sectic
that we could define a number of quite complicated lists, like the list of prime number
and lists of random numbers. The question remains, though, of whether these listsa
anything other than a curiosity. There are two arguments which show their irnporta~
in functional programming.
First, an intinite version of a program can be more abstract, and so simpler
write. Consider the problem of finding the nth prime number, using the Sieve I
Eratosthenes. If we work with finite lists, we need to know in advance how large ali
is needed to accommodate the first n primes; if we work with an infinite list. this isn
necessary: only that part of the list which is needed will be generated as con~putatic
proceeds.
In a similar way. the random numbers given by randomsequence seed provided;
unlimited resource: we can take as many random numbers from the list as we requir
There needs to be no decision at the start of programming as to the size of sequea
needed. (These arguments are rather like those for virtual memory in a computer.
is often the case that predicting the memory use of a program is possible, but tiresom
virtual memory makes this unnecessary. and so frees the programmer to proceed wi
other tasks.)
The second argument is of wider significance, and can be seen by re-examining tl
way in which we generated random numbers. We generated an infinite list by mea
of iterate, and we transformed the values using map; these operations are pictur
Why infinite lists? 371
I X, y, z,... I I g x,g y, g
2, . . . I
L - - - - - - - J L - - - - - - - - - - J
in Figure 17.3 as a generator of and a transformer of lists of values. These values are
shown in the dashed boxes. These components can then be linked together, giving more
complex combinations, as in Figure 17.4. This approach modularizes the generation
of values i n a distribution in an interesting way. We have separated the generation of the
values from their transformation, and this means we can change each part independently
of the other.
Once we have seen the view of infinite lists as the links between processes, other
combinations suggest themselves, and in particular we can begin to write process-style
programs which involve recursion.
Among the exercises in the last section was the problem of finding the running sums
of the list [ao , a 1 ,a 2 , . . .. Given the sum up to ak, say, we get the next sum by
adding the next value in the input, ak+1. It is as if we j>ed the sum buck into the
process to have the value a k + l added. This is precisely the effect of the network of
processes in Figure 17.5, where the values passing along the links are shown in the
dotted boxes.
The tirst value in the output o u t is 0, and we get the remaining values by adding the
next value in i L i s t to the previous sum, appearing in the list o u t . This is translated
into Haskell as follows. The output of the function on input i L i s t is out. This is
372 Lazy programming
L
X, y, z,. . .
- ~ - - - -
I
- .---
I X, x+y, x+y+z,. . .
r------'
I
itself got by adding 0 to the front of the output from the zipwith (+I, which itselfha
inputs iList and out. In other words,
and the operator section (0:) puts a zero on thc front of a list. We give a calculation
of an example now.
-
listsums [I . . ]
?-r
out
0 : zipwith (+) [I . . ] out
I- 0 : zipwith (+) [l .. 1 (0:. . .) (1)
0 : 1+0 : zipwith (+) 12 . . ] (1+0:...) (2)
--. 0 : 1 : 2+1 : zipwith (+) [3 . . 1 (2+1:.. . ) --t .. .
In making this calculation, we replace the occurrence of out in line (1) with the
incomplete list (0:. . . ) . In a similar way, we replace the tail of out by (1+0:...)
in line (2).
The definition of listsums is an example of the general function scanll'. which
combines values using the function f , and whose first output is st.
Case study: simulation 373
Exercises
17.27 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.28 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'?
Section 14.5, where we designed the algebraic types Inmess and Outmess,
Section 16.5, where the abstract types Q u e u e s t a t e and S e r v e r s t a t e were intro-
duced, 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 slid 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
374 Lazy programming
where the first parameter is the state of the server at the start of the simulation. In
Section 16.5 we presented the function performing one step of the simulation,
which takes the current server state, and the input message arriving at the current minute
and returns the state after one minute's processing, paired with the list of the output
messages produced by the queues that minute (potentially every queue could releasea
I
customer at the same instant. just as no customers might be released.)
The output of the sinlulation will be given by the output messages generated in the
first minute, and after those the results of a new simulation beginning with the updaled
state:
How do we generate an input sequence? From Section 17.6 we have the sequence of
times given by
randomTimes
-
= map (makeFunction dist
[ 2 , 5 , 1 , 4 , 3 , 1 , 2 , 5 , ...
. fromInt) (randomsequence seed)
We are to have arrivals of one person per minute, so the input messages we generate
are
simulationInput
-
= zipwith Yes [ I . . 1 randomTimes
[ Y e s l 2 , Y e s 2 5 , Y e s 3 1 , Y e s 4 4 , Y e s 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
The first six inputs are processed without delay, but the seventh requires a waiting time
of 2 before being served.
The infinite number of arrivals represented by simulationInput will obviously
generate a corresponding infinite number of output messages. We can make a finite
approximation by giving the input
Proof revisited 375
where after one arrival in each of the first 50 minutes. no further people arrive. Fifty
output messages will be generated, and we define this list of outputs thus:
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
For simulationInput2 the total waiting time is 29, going up to 287 with three queues
and down to zero with five. We leave it to the reader to experiment with the round
robin simulation outlined in the exercises of Section 16.5.
A more substantial project is to model a set-up with a single queue feeding n number
of bank clerks - one way to do this is to extend the serverstate with an extra queue
which feeds into the individual queues: an element leaves the feeder queue when one
of the small queues is empty. This should avoid the unnecessary waiting time we face
when making the wrong choice of queue, and the simulation shows that waiting times
are reduced by this strategy, though by less than we might expect if service times are
short.
j179)Proof revisited
After summarizing the effect that lazy evaluation has on the types of Haskell. we
examine the consequences for reasoning about programs. Taking lists as a representative
example, we look at how we can prove properties of infinite lists, and of all lists. rather
than simply the set of finite lists, which was the scope of the proofs we looked at in
Chapters 8, 10 and 14.
This section cannot give complete coverage of the issues of verification; we conclude
with pointers to further reading.
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
376 Lazy programming
undef : : a
undef = undef (undef .l)
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
fak n = (n+l) * fak n
where we have confused the use of n and n+l in attempting to define the factorial
function. The value of fak n will he the same as undef, as they are both non,
terminating.
We should remark that we are using the term 'undefined' in two different ways here
The name undef is given a definition by (undef . I ) ; the value that the definitior
gives it is the undefined value, which represents the result of a calculation or evaluatior
which fails to terminate (and therefore fails to define a result).
The existence of these undefined values has an effect on the type of lists. What ifw
define, for example, the list
listl = 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 [Intl therefore contains partial lists like listl
built from the undefined list, undef, parts of which are defined and parts of whicharl
not.
Of course, there are also undefined integers, so we also include in [Intl lists like
list2 = undef: [2,3]
list3 = undef:4:undef
which contain undefined values, and might also be partial. Note that in list3 the fin
occurrence of undef is at type Int while the second is at type [Intl.
What happens when a function is applied to undef? We use the rules for calculatio
const 17 undef -
we have seen already, so that the const function of the standard prelude satisfies
17
If the function applied to undef has to pattern match, then the result of the functio
will be undef, since the pattern match has to look at the structure of undef,which wi
never terminate. For instance, for the functions used in Chapter 8,
sum undef
doubleAll undef -
--+ undef
undef
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.
Proof revisited 377
for all finite lists xs,ys and 2s. For these results to hold for all fp-lists, we need to
show that
sum (doubleAll undef) = 2 * sum undef (sum-doub1e.u)
undef ++ (ys ++ 2s) = (undef ++ ys) ++ zs (assoc++.u)
reverse (undef ++ ys) = reverse ys ++ reverse undef (reverse++.u)
as well as being sure that the induction step is valid for all fp-lists. Now, by (sum.u) and
(doub1eAll.u) the equation (sum-doub1e.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 detinition of reverse:
reverse [I = [I
reverse (x:xs) = reverse xs ++ [XI
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
reverse undef = undef
Taking a defined list, like [2,3] for ys in (reverse++.u) gives
reverse (undef ++ [2,31)
= reverse undef
= undef
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.
378 Lazy programming
Infinite lists
Beside the fp-lists, there are infinite members of the list types. How can we pro\
properties of infinite lists? A hint is given by our discussion of printing the results~
evaluating an infinite list. In practice what happens is that we interrupt evaluation t
hitting Ctrl-C after some period of time. We can think of what we see on the screc
as an approximation to the infinite list.
If what we see are the elements a0 ,a1 ,a2, . . . ,an,we can think of the approxim,
tion being the list
since we have no information about the list beyond the element an.
More formally, we say that the partial lists
undef, ag:undef, ao:al:undef, ao:al:a2:undef, ...
.
are approximations to the infinite list [a0 ,a1 ,a2,. . ,an,. . . I .
Two lists xs and ys are equal if all their approximants are equal, that is for all natur
numbers n, take n xs = take n ys. (The take function gives the defined portic
of the nth approximant, and it is enough to compare these parts.) A more usable versic
of this principle applies to infinite lists only.
)
p~
Example
Assuming these lists are infinite (which they clearly are), we have to prove for all natu
numbers n that
facMap! !n = facs! !n (facMap . !
Proof revisited 379
Proof In our proof we will assume for all natural numbers n the results
(map f x s ) ! ! n = f (xs ! ! n) (map. ! ! )
(zipwith g x s y s ) ! ! n = g ( x s ! ! n ) ( y s ! ! n ) (zipwith. ! !)
which we discuss again later in this section.
(f acMap . ! ! ) 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-I.
Base We start by proving the result at zero. Examining the left-hand side first,
f acMap ! ! 0
= (map f a c [O . . 1 ) ! ! 0 by (f acMap. 1)
= f a c ([O . . 1 ! ! 0 ) by (map. ! ! )
= fac 0 b y d e f o f [O . . I , ! !
= 1 by ( f a c . 1)
The right-hand side is
f a c s ! !O
= (1 : z i p w i t h (*) [ I .. 1 facs) ! !0 by (f a c s . 1)
= I by defof ! !
thus establishing the base case.
Induction In the induction case we have to prove (f acMap. ! ! ) using the induction
hypothesis:
f a c ~ a p!!(n-1) = f a c s ! ! (n-I)
The left-hand side of (f acMap. ! ! ) is
f acMap ! ! n
= (map f a c [O . . I ) ! ! n by (facMap. 1)
= f a c ([O . . I ! ! n ) by (map. ! ! )
= fac n b y d e f o f [O . . I , ! !
= n * f a c (n-I) by ( f a c . 2 )
It is not hard to see that we have facMap ! ! (n-1) = f a c (n-1) by a similar
argument to the first three steps here and so,
= n * (facMap! ! (n-1))
The right-hand side of (f acMap . ! ! ) is
f a c s ! !n
= (1 : z i p w i t h ( * ) [ I . . 1 f a c s ) ! !n by (f a c s . 1)
= ( z i p w i t h ( * ) [ I . . I f a c s ) ! ! (n-I) by def of ! !
= (*) ( [ I . . I ! ! ( n - I ) ) ( f a c s ! ! ( n - I ) ) by (zipwith. ! ! )
= ( [ I . . I!!(n-1)) * (facs!!(n-1)) by def of ( * )
= n * ( f a c s ! ! (n-1)) bydefof [ I . . I , ! !
= n * (facMap! ! ( n - I ) ) by ( ~ Y P )
The final step of this proof is given by the induction hypothew, and completes the
proof of the induction step and the result itself. rn
380 Lazy programming
Further reading
The techniques we have given here provide a flavour of how to write proofs for infinit
lists and inlinite data structures in general. We cannot give the breadth or depth of
full presentation. but refer the reader to Paulson (1987) for more details. An alternativ
approach to proving the fktorial list example is given in 'Thompson ( 1999). which als
gives a survey of proof in functional programming.
Exercises
(map f [m . . I ) ! !n = f (m+n)
Proof revisited 381
[Hint: you will need to choose the right variable for the induction proof.]
17.33 Prove that the lists
f acMap = map f a c [O . . ]
f a c s = 1 : z i p w i t h (*) [I .. 1 facs
are infinite.
17.34 If we define indexing thus
x :0 = X
( - : x s ) ! ! n = x s ! ! (n-I)
[]! ! n = e r r o r "Indexing"
(map f x s ) ! ! n = f ( x s ! !n)
and therefore infer that the result is valid for all lists xs. State and prove a similar
result for zipwith.
17.35 Show that the following equations hold between functions.
f i l t e r p . map f = map f . f i l t e r (p . f)
f i l t e r p . f i l t e r q = f i l t e r (q &&& p)
concat . map (map f ) = map f . concat
[ Summary )
L a ~ evaluation
y of Haskell expressions means that we can write progranls in a different
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. I n finding
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 progranls whose purpose is clear and whose execution is efficient.
We re-examined the list comprehension notation, which makes many list processing
programs easier to express: we saw this in the particular examples of route finding and
parsing.
A design principle exploited i n 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. [I. This 'list of successes' method
is useful i n a variety of contexts.
382 Lazy programming
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 1
Programming with
actions
18.1 Why is I10 an issue?
18.2 The basics of inputloutput
18.3 The do notation
18.4 Iteration and recursion
18.5 The calculator
18.6 Further I10
18.7 The do construct revisited
18.8 Monads for functional programming
18.9 Example: monadic computation over trees
The programs we have written so far in this book have been self-contained. However,
most larger-scale programs have some interaction with the 'world outside'. This can take
many forms.
A program, like the Hugs interpreter itself, can read from a terminal and write to a
terminal.
A mail system reads and writes from files as well as standard terminal channels.
An operating system executes programs in parallel, as well as controlling devices like
printers, CD-ROM readers and terminals.
This chapter explores 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.
384 Programming with actions
We begin the chapter by discussing how in the past I10 has been a problem for the
users of a functional language. The solution in Haskell is to introduce the types 10 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, a
well complex programs which are built from a number of 10 programs sequenced into
one by means of the do construct.
We show a number of examples of interactive programs, including an interactii
version of the calculator case study, and also discuss some of the more general I10
facilities in the standard prelude and libraries.
The sequential nature of the 10 a types is not peculiar to I/O and in the second hall
of the chapter we show how these types are simply one example of the more general
phenomenon of a monad; other examples include side-effects, error-handling and non.
determinacy.
We argue that monads provide a powerful structuring mechanism for functional pro.
grams incorporating these effects, as well as providing an interface between the func.
tional and imperative worlds. We illustrate this versatility by showing that two substan,
tially different programs over a tree will have the same top-level structure if they art
programmed in a monadic style.
v a l : : Int
v a l = 42
The effect uf thcxc' definitions is to associate a fixed value with each name: in the casl
o f v a l the value is an integer and i n the case o f function i t i s a function from integer
to integers. How is an input or an output action to fit into this model'?
One approach -taken in Standard ML (Milner rt al. 1997), for instance - i s to includ
operations like
inputInt : : Int
whose effect is to read an integer from the input; the value read in becomes the valu
given to inputInt. Each time inputInt is evaluated i t will be given a new value, and
so i t is not a fixed integer value as i t ought to be according to our original model.
I
Allowing this operation into our language may not seem to cause too big a problem,
but examining the example o f
shows how i t has two important consequences for our model o f functional programming.
The basics of inputloutput 385
Suppose that the tirst 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.
More seriously, (inputDiff .1) breaks the model of reasoning which we have used.
We would hitherto have expected that subtracting a value from itself would have given
a result of 0, but that is not the case here.
The reason for this is precisely that the meaning of an expression is no longer
determined by looking only at the meanings of its parts, since we cannot give a
meaning to inputInt without knowing where it occurs in a program; as we saw in
the previous point, the first and second occurrences of input Int in inputDiff will
generally have different values.
As the second point shows, if we take this approach then it will be substantially more
difficult to understand the meaning of any program. This is because m y definition in
a program may be affected by the presence of the I10 operations. An example is the
function
funny : : Int -> Int
funny n = inputInt + n
from whose definition we can see the dependence on 110, but potentially any function
may be affected in a similar way.
Because of this, YO proved to be a thorny issue for functional programmers for some
considerable time, and there have been a number of attempts to find the right model for
110 - indeed, earlier versions of Haskell included two of these. An illuminating history
and overview of functional 110 is given in Gordon (1994).
This chapter describes the monadic approach, which has proved to be a robust model
that extends easily to other sorts of interaction with the 'world outside'. The basic idea
of monadic I10 is to control how programs that perform 110 are built, and in particular
to limit the way that the I10 operations affect functions in general. This is the topic of
the next section.
how to put these components together using the do notation to form more complex U0
programs.
Reading input
The operation which reads a line of text from the standard input does some 110 and
returns a S t r i n g which is the line just read. According to the explanation above, this
should be an object of type 10 S t r i n g , and indeed, the built-in function
getLine : : I 0 S t r i n g
g e t c h a r : : I 0 Char
I0 0
and they will return the value 0 as their result.
Writing Strings
The operation of writing the string "Hello, World! " will be an object which performs
some 110, but which has nothing of significance to pass back to the program. It is
therefore of type 10 0.
The general operation to print a text string will be a function which takes the strinf
to be written, and gives back the 110 object which writes that string:
p u t S t r : : S t r i n g -> I 0 ()
putStrLn : : S t r i n g -> I 0 ()
putStrLn = p u t S t r . (++ " \ n u )
The effect of this is to add a newline to the end of its input before passing it to putsti-.
The do notation 387
Here we see the effect of do is to sequence a number of I0 actions into a single actio~
The syntax of do is governed by the offside rule, and do can take any number c
arguments. We see an example of more arguments next.
2. We can write an I/O program to print something four times. The first version1
this is
3. Rather than 'hard wiring7 the number of times to output the string, we can make
this a parameter of the program,
put4times = putNtimes 4
4. 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. I' thus:
The do notation 389
read2lines :: I0 ()
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 110
program which reads an arbitrary number of lines.
( Examples ]
5 . 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 110 program'? As part of
a do program we can name the results of I0 a actions. A program to read a line and
then write that line is given by
getNput : : I0 ( )
getNput = do line <- getLine
putStrLn line
line := getLine
but you should be aware that there arc important differences between the names in
a Haskell 110 program and the variables in an imperative program. The essential
difference is that each 'var <-' creates a new variable var, and so the lang~~age
pennits 'single assignment' rather than the 'updatable assignment' familiar from the
vast majority of modern imperative languages; we will say more about this difference
in Section 1 8.4.
6. We are not forced simply to output the lines we have read, unchanged, so that we
might define
390 Programming with actions
reverse2lines : : I0 ( )
reverse2lines
= do linel <- getLine
line2 <- getLine
putStrLn (reverse line21
putStrLn (reverse linel)
In this example, we read two lines, and then write them in the opposite order, reversed
[ Example )
7. Example 6 can be redefined to contain local definitions of the reversed lines
reverse2lines : : I0 ()
reverse2lines
= do linel <- getLine
line2 <- getLine
let revl = reverse linel
let rev2 = reverse line2
putStrLn rev2
putStrLn revl
which can be used to parse a string representing a value of a particular type into that
value.
Example
but then we need to sequence this with an I10 action to return the line interpreted as
an Int. We can convert the line to an integer by the expression
Iteration and recursion 391
read l i n e : : I n t
What we need is the I 0 I n t action which returns this value - this is the purpose of
r e t u r n introduced in the previous section. Our program to read an I n t is therefore
getInt : : I0 I n t
g e t I n t = do l i n e <- getLine
r e t u r n (read l i n e :: I n t )
Summary
This section has shown that a do expression provides a context in which to do sequential
programming. It is po9sible to program complicated 110 interactions, by sequencing
simpler 110 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 I10 program. It is also
possible to make these programs more readable by judicious use of l e t definitions to
name intermediate calculations.
In the next section we look at how to write repetitive 110 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 differently from
the usual assignment operator.
18.1 Write an I10 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.
18.2 Write an 110 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.
18.3 Write an I10 program which will first read a positive integer, n say. and then
read n integers and write their sum. The program should prompt appropriately
for its inputs and explain its output.
A while loop
Suppose that we want to repeat an I 0 (1 action while a condition is true. The condition
will depend upon the UO system, and so will be of type
392 Programming with actions
An example of this, which is provided in the library module 10.hs,is a test for theend
of input,
This discussion means that our while-loopconstruct will have the type
while : : I0 Boo1 -> I0 () -> I0 ()
which is a sequence of two I 0 actions. Tn the first we perform the test,and its Boolea
result is named res. The second action is conditional on the value of res; if resi
True then the action performed is
do action
while test action
This means that in the then case the effect is first to perform the action and then I
repeat the loop. On the other hand, if the condition is False the effect of the progm
should be to 'do nothing'. The null I10 action is
return 0
What action do we want to do if there is still input to be read? We read then write, as
we saw earlier,
do line <- getLine
putStrLn line
Iteration and recursion 393
An important example
Suppose now that we want to copy lines o f input until we hit a line which is empty.
when we stop. A first attempt might be
goUntilEmpty : : I0 ( )
goUntilEmpty
= do line <- getLine
while (return (line /= [I))
(do putStrLn line
line <- getLine
return 0 )
where the lines have been numbered to make discussion easier. The apparent effect ol'
thi\ program is as follow\. At (1) a line is read, and named line: while this line is
not empty -the test return (line /= [I ) at (2) - we output line at (3) and read
another line into line at (4).
The effect of this program is repeatedly to write out the jrst line of the input; that
is the line read in at (1) and used i n (2) and (3). In (4)we create a new variable
line and associate the value read with it, but this is not a re-assignment to the original
variable. and so the test in (2) and the print in (3) still refer to the first line. It is i n
this way that these variables differ from the variable\ of an imperative programming
language: if we think of
line <- getLine
as an assignment line := getLine then it is a sirzgle assignntctzt to a variable which
cannot be updated: every occurrence of line <- . . . creates a new variable. In other
words, the variables here do not change their values.
How can we think of writing a correct program to this specification'! The key is to
think recursively.
goUntilEmpty : : I0 0
goUntilEmpty
= do line <- getLine
if (line == [ I )
then return ( )
else (do putStrLn line
goUntilEmpty)
Programming with actions
The effect here is to get a line, ( I ) , and if that line is empty, (2). the 110 actionshd
with the return 0 in ( 3 ) . On the other hand, if the line is not empty it is outpurl
(4). This is followed by the whole program being reinvoked, and when this is do
the next line read is called line,and if that is not empty, (2). (4) and (5) are repeati
for this new line.
sumInts : : I 0 Int
which returns this sum. In writing the program there are two cases: if we read zem
then the result must be zero; if not, we get the result by adding the number just read to
the sum of the remaining lines, which is given by calling sumInts again. This gives
the program
sumInts
= do n <- getInt
if n==O
then return 0
else (do m <- sumInts
return (n+m) )
where we use the getInt function defined earlier to read a single Int on a line ofits
own. It is interesting with compare this with the recursion in
sum [I = 0
sum (n:ns)
= n + sum ns
sum C1 = 0
sum (n:ns)
= let m = sum ns
in (n + m)
We can also put the sumInts program inside a 'wrapper' which explains its purpose
and prints the sum at the end.
sumInteract : : I0 0
sumInteract
= do putStrLn "Enter integers one per line"
putStrLn "These will be summed until zero is entered"
sum <- sumInts
putStr "The sum was "
print sum
Iteration and recursion 395
Exercises ]
4 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.
18.5 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?
18.6 Give a definition of the function
so that repeat t e s t oper has the effect of repeating oper until the condition
t e s t is True.
18.8 Give a generalization of while in which the condition and the operation work
over values of type a. Its new type is
18.9 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.
18.10 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.
18.11 Define a function
The calculator
The ingredients of the calculator are contained in three places in the text.
In Section 14.2 we saw the introduction of the algebraic type of expressions, Expr,
which we subsequently revised in Section 17.5, giving
i n i t i a l :: Store
value : : S t o r e -> Var -> I n t
update : : S t o r e -> Var -> I n t -> S t o r e
which is used to parse each line of input into a Command. For instance,
e v a l ( L i t n) s t = n
e v a l (Var v) s t = value st v
e v a l (Op op e l e2) s t
= opValue op v l v2
where
v l = e v a l e l st
v2 = e v a l e2 st
The calculator 397
calcStep st
= do line <- getLine
let comm = commLine line
(va1,newSt) = command comm st
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 (I),
let comm = commLine line
gives comm the value of parsing the line,and this is subsequently used in (2).
(va1,newSt) = command comm st
which sin~ultaneouslygivcs val and newst the value of the expression read and the
new state. Note that the let extends over multiple lines, and that it is terminated by
the symbol 'print'. In the lines that follow the let,the value val is printed and thc
new state newst is returned as the overall result of the interaction.
A sequence of calculator steps is given by
calcsteps st
= while notEOF
(do newst <- calcStep st
calcsteps newst)
398 Programming with actions
18.12 How would you add initial and final messages to the output of the calculator:
18.13 If the calculator is not given a valid command, then an error message will
generated by the function t o p l e v e l , and evaluation stops. Discuss how yl
would add an extra argument to topLevel to be used in the error case. so tt
evaluation with the calculator does not halt.
18.14 Discuss how you would have to modify the system to allow variables to ha
arbitrarily long names, consisting of letters and numbers, starting with a lette
18.15 How would you extend the calculator to deal with decimal floating-point numbc
as well as integers?
18.16 Discuss how you would modify the calculator so that it could read input COI
mands split over more than one line. You will need to decide how this sort
split is signalled by the user - maybe by \ at the end of the line - and how
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.1 7 How would you modify the parser so that 'white space' is permitted in the input
commands, as in the example
It
x : (2\t+3) I1
@ Further 110
In this section we survey further features of Haskell 110.
Further 110 399
File I/O
So far we have seen that we can read from the terminal - the 'standard input' - and
write to the screen - the 'standard output'. The Haskell 110 model also provides for
reading from and writing and appending to files, by means of the functions
readFile :: FilePath -> I0 String
writeFile : : FilePath -> String -> I0 (1
appendFile : : FilePath -> String -> I0 0
where
type FilePath = String
and files are specified by the text strings appropriate to the implementation in question.
Errors
110 programscan raiseerrors. which belong to the system-dependent datatype IOError,
The function
ioError : : IOError -> I0 a
builds an I10 action which fails giving the appropriate error, and the program
catch : : I0 a -> (IOError -> I0 a) -> 10 a
will catch an error raised by the first argument and handle it using the second argument.
which gives a handler - that is an action of type I0 a - for each possible IOError.
More details of error handling can be found in the documentation for the I10 library
10.hs.
g e t c o n t e n t s : : 10 S t r i n g
a primitive t o get the contents of the standard input, and which is used in
i n t e r a c t : : ( S t r i n g -> S t r i n g ) -> I 0 0
i n t e r a c t f = do s <- g e t c o n t e n t s
p u t S t r (f s)
f Exercises I
18.18 Write file-handling versions of the programs goUntilEmpty and sumInts.
18.19 Write lazy-list versions of thc programs goUntilEmpty and sumInts.
18.20 [Harder] Write a lazy-list version of the calculator program.
with a function taking the result of this (of type a) into an 10 b, that is an object of
type a -> I 0 b,
Monads for functional programming 401
We can join them together, passing the result of the first as an argument to the second,
thus:
The result of putting them together i s something which does some I10 before returning
a value of type b:
( Exercise 1
18.21 Repeat some of the earlier examples and exercises using the >>=operatorinstead
of a do expression.
When is such a sequencing necessary'? Consider the example of the numeric expression
e - f
As this is simply an expression, we can choose to evaluate the arguments to -, e and
f , in either order, or indeed in parallel. Suppose, however, that the expressions e ad
f cause some 110 to take place, or cause some store to be changed. Then we needto
say in which order the evaluation takes place, since different orders will give different
results. A simple example. first discussed in Section 18.1, is
What is a monad?
A monad is a family of types m a, based on a polymorphic type constructor m, wid
functions r e t u r n , (>>=I, (>>I, and f a i l :
class Monad m where
(>>=) : : m a -> (a -> m b) -> m b
r e t u r n : : a -> m a
(>>) : : m a -> m b -> m b
fail : : S t r i n g -> m a
This is an example of a constructor class, which is like a type class, except that th
things which belong to a constructor class are type constructors - that is function
which build types from types - rather than types. Examples of type constructors ar
'list', written [I in Haskell, and I0 as we have seen already.
The definition of Monad also contains default declarations for >> and fail:
Monads for functional programming 403
to give
do y <- r e t u r n x = f x
f Y
do x <- m = m
return x
and the third is implicit in the fact that the do construct is associative.
m>>=f = f m
return = id
Under this interpretation, >@>becomes forward co~npositionof functions, >. >, which
is indeed associative and has i d as its identity. An undefined computation sequenced
with any other computation will be undefined.
Computationally, this monad represents the trivial state in which no actions are
performed, and values are returned immediately.
i n s t a n c e Monad [I where
xs >>= f = concat (map f x s )
r e t u r n x = Cxl
fail s = [I
Monads for functional programming 405
s p a r s e : : SParse a b -> P a r s e a b
s p a r s e (SParse p r ) = p r
The crux of the def nition of (>>=I is like that of (>*>I - a parse is done by one parser,
pr, and the remains of the input are passed to a second parser f , here dependent on
the result of the first parse, and so a result of the first parse, x, is passed to f to give a
second parser, which is applied to the remaining input, r e s t .
Combining monads
The monads here can be combined to give more complex effects, so that one can build
computations which perform 110 and inanipulate a state value, for instance. A full
account of how this can be done in a systematic way is given in Liang, Hudak andJones
( 1995).
mapF f m
= do x <- m
return (f x)
joinM m
= do x <- m
X
Over lists these functions are called map and concat;many of the properties ofmapand
concat over lists lift to these functions. For instance, we can show using properties
(MI) to (M3) that for all f and g
mapF (f . g ) = mapF f . mapF g (M4)
Exercises
18.22 Show that sets and binary trees can be given a monad structure, as can the type
18.23 For the monads Id, [I and Maybe prove the rules (MI) to (M3). Also show tha
these rules hold for your implementations in the previous exercise.
18.24 Prove the property (M4) using the laws (MI) to (M3).
18.25 Prove the following properties using the monad laws:
18.26 Can you define a different monad structure over lists from that given above
Check that your definition has properties (MI) t o (M3).
Example: monadic computation over trees 407
18.27 Write down the definitions ofmap and j o i n over lists using list comprehensions.
Compare them with the definitions of mapF and joinM given in the do notation
in this section.
18.28 Reimplement the parser for the calculator using the do construct (based on >>=)
rather than (>*>I and build. Contrast the two approaches.
can be given a monadic structure. We first look at a simple example, and then we look
at a rather more realistic one.
We see that with a monadic approach the top-level structure of the two solutions is
exactly the same. This structure guides the way that we build the implementation of
the second example, as we shall see.
The moral of these examples is that monads provide an important structuring mechan-
ism for program construction, as they encourage a separation of concerns. The top-
level structure of the computation is given in terms of a monad whose specitic properties
are only touched upon. Within the monad itself is the appropriate computational
behaviour to, for example, maintain a state or to perform some I 0 (or both); the particular
sequencing operation of the monad will ensure that values are passed between the parts
of the program in an appropriate way.
This separation of concerns comes into its own when changes are required in the
details o f the computation: it is usually possible to change the monad implementing a
computation with at most minimal changes requircd at the top level. This is in stark
contrast to a non-monadic computation in which data representations are visible: a
wholesale restructuring is often required in such a situation.
sTree N i l = 0
sTree (Node n tl t2) = n + sTree t l + sTree t 2
In writing this we give no explicit sequence to the calculation of the sum: we could
calculate sTree t l and sTree t2 one after the other, or indeed in parallel. How might
a monadic solution proceed?
I
Moon
Ahmet Moon
t y p e Table a = [a1
where the table [True , F a l s e ] indicates that True is associated with 0 and Falsewitl
1.
What then is the state monad'? It consists of functions
d a t a S t a t e a b = S t a t e (Table a -> (Table a , b ) )
which. after we strip off the constructor S t a t e , we can think of as taking the star
before doing the operation to the state afier the operation, together with its result. 11
other words, we return a value of type b, but perhaps we change the value of the stat1
of type Table a as a side-effect.
Next we have to define the two monad operations.
i n s t a n c e Monad ( S t a t e a ) where
r e t u r n x = S t a t e ( \ t a b -> ( t a b , x ) )
How do we sequence the operations? The intended effect here is to do st, pass its resu
to f and then do the resulting operation.
In more detail, to perform s t , we pass it the table t a b ; the output of this is a nei
state, newTab, and a value y. This y is passed to f , giving an object of type State a 1
this is then performed starting with the new state newTab.
( S t a t e s t ) >>= f
= S t a t e ( \ t a b -> l e t
(new~ab,~) = s t t a b
(State trans) = f y
in
t r a n s newTab)
Here we can see that the operations are indeed done in sequence. leading from one st8
value to the next. This has given us the monad; all that remains is to define the functit
numberNode. Our definition is
This has to perform the calculation, starting with some initial table, and rcturn the
resulting value of type u. The definition is
e x t r a c t : : S t a t e a b -> b
e x t r a c t ( S t a t e s t ) = snd ( s t [I)
where we see that s t is applied to the initial statc [I. The result of this is a pair. from
which we select the second part, of type b. Now we can define our function
numTree : : E q a => Tree a -> Tree I n t
nwnTree = e x t r a c t . numberTree
where the extra integer parameter carries the current 'offset' into the list.
18.30 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 Section
17.6.
412 Programming with actions
18.31 We can use monads to extend the case study of the calculator in a variety of
ways. Consider how you would
Summary
Looking at the examples we have covered, we can conclude that the advantages of
structuring a computation using a monad are threefold:
We follow a well-defined strategy for writing sequential programs. which has stmy
similarities with imperative programming.
These is an advantage in abstraction: we can change the underlying inonad ye1
retain the overall structure of the computation.
Finally, we have seen that various properties can be inferred automatically once we
have a monad. As we saw above, (M4) is a consequence of the monad propertier
(MI) to (M3).
We have also seen that many different sort of computational effects like 10, state, error!
(as implemented by the Maybe type) and non-determinism, as given by the list monad
are all described by means of monads. On the one hand this means that we can makt
models in a functional language of these effects, but on the other we can use monad!
as a way of building an interface between a pure functional language like Haskel
and systems with effects; this approach appears to be very fruitful, allowing Haskel
programs to call foreign-language functions, and indeed allowing Haskell programmer:
to inter-work with programmers in C and Java. We give some pointers to this workanr
also to further work on programming in a monadic style in the concluding chapter.
Time and space
behaviour
19.1 Complexity of functions
19.2 The complexity of calculations
19.3 Implementationsof sets
19.4 Space behaviour
19.5 Folding revisited
19.6 Avoiding recomputation: memoization
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 programs. We
work out the time complexity of a sequence of functions, leading up to looking at various
implementations of the S e t 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 discussion
of folding functions into lists, and we introduce the f old1 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.
414 Time and space behaviour
a Complexity of functions
If we are trying to measure the behaviour of functions, one approach i \ to ask how much
time and space are consumed in evaluations for different input values. We might, fo~
example, given a function f red over the natural nulnbers, count the number of step
taken in calculating the value off red n for natural numbers n. This gives usafunction,
call it stepsFred, and then we can ask how complex that function is.
One way of estimating the complexity of a function is to look at how fast it grows
for large values of its argument. The idea of this is that the essential behaviour of!
function becomes clearer for large values. To start with, we examine this idea througt
an example. How fast does the function
a constant 13,
a term 4*n, and
a term, 2*n2. (Note that here we use the mathematical notation for n2, rathe~
than the Haskell notation, n-2.)
For 'large' values of n the square term is greater than the others, and so we say that f i!
of order n2, 0 (n2). In this case the square dominates for any n greater than or equa
to 3; we shall say exactly what is meant by 'large' when we make the definitiono
order precise. As a rule of thumb we can say that order classifies how functions behavr
when all but the fastest-growing components are removed, and constant multipliers an
ignored; the remainder of the section makes this precise, but this explanation shouldh
sufficient for understanding the remainder of the chapter.
The notation n2 is the usual way that mathematicians write down 'the function tha
takes n to n2'. This is the notation which is generally used in describing complexity
and so we use it here. In a Haskell program to describe the function we would eithe
write \n -> n-2 or use the operator section (-2).
In the remainder of this section we make the idea of order precise, before examinin1
various examples and placing them on a scale for measuring complexity.
The definition expresses the fact that when numbers are large enough (n>m) the value
o f f is no larger than a multiple of the function g, namely (d*) .g.
For example, f above is 0(n2) since, for n greater than or equal to 1,
A scale of measurement
We say that f << g i f f is O(g), but g is not O(f ); we also use f
is 0 (g) and simultaneously g is 0 (f 1.
-
g to mean that f
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 l o g n,
n(1og n) fits between linear and quadratic, thus
no << log n << n1 << n ( l o g n) << n2 << . . .
Counting
Many of the arguments we make will involve counting. In this section we look at somc
general examples which we will come across i n examining the behaviour of functions
below.
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 oneL?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') I n > (2(p-l))
which when we take log2 of each side gives
The function giving the number of steps in terms of the length of the list, n, will thus
be @(log2 n).
416 Time and space behaviour
2. The second question concerns trees. A tree is called balanced if a11 its branches
are the same length. Suppose we have a balanced binary tree, whose branches areof
length b; how many nodes does the Lree have? On the first level it has 1, on the second
2, on the kth it has 2(k-1), so over all b + l levels it has
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
C1 . . nl, i n other words? Writing the list backwards, as well as forwards, we have
which lnakes it @(n2), or quadratic. In a similar way, the sum of the squares is @(n3),
and so on.
Complexity of functions 41 7
f Exercises 1
19.1 Show that the example
is @(n2).
19.2 Give a table of the values of the functions no,log n,n', n(log n), n2,n3 and
2" for the values
19.3 By giving the values of d, m and c (when necessary), show that the following
functions have the complexity indicated.
19.4 Show that nk << 2" for all positive k. By taking logarithms of both sides, show
that log n << nk for all positive k.
19.5 Show that
log - In = log2
and in fact that logarithms to any base have the same rate of growth.
19.6 The function f i b is defined by
fib 0 = 0
fib 1 = 1
f i b m = f i b (m-2) + f i b (m-I)
-
19.7 Show that << is transitive - that is f <<g and g<<h together imply that f <<h.
Show also that is an equivalence relation.
19.8 If f is 0 (g), show that any constant multiple of f is also of the same order. If
f 1 and f 2 are 0 (g), show that their sum and difference are also 0 (g). Are the
same results valid with Q replacing O'?
19.9 If f 1 is o (nkl) and f 2 is 0 (nk21, show that their product,
Complexity measures
We measure thecomplexity of the function f by looking at the time and spacecomplexit
as described above. as , f u r d o n s of the size of the inputs to f . The ~ i z of
e a numbe
is the number itself, while the size of a list is given by its length, and of a tree by th
number of nodes it contains. We now look at a series of examples.
-
4
fac n
n *
...
f a c (n-1)
The complexity of calculations 419
The calculation contains 2*n+l steps, and the largest expression, ( f acMax), contains
n multiplication symbols. This makes the time and space complexity both @ ( n i l , or
linear.
i n s x [I = [XI
ins x (y:ys)
I (x<=y> = x:y:ys
I otherwise = y : i n s x ys
-
-.A
-.A
i n s a 1 ( i S o r t [ a 2 , . . . ,an-1 , a n ] )
...
i n s a 1 ( i n s a2 ( ... ( i n s an-1 ( i n s an [ I ) ) . . .))
followed by the calculation of the n ins's. What sort of behaviour does i n s have?
Take the general example of
steps, whose sum is again o ( n 2 ) , by our observation in Section 19.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.
420 Time and space behaviour
3. Before looking at another sorting algorithm, we look at the time taken to join
together two lists, using ++.
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 sorte
case. In the average case, we split thus
where the list has been bisected. Forming the two sublists will take O(nl) steps, a
will the joining together of the results. As we argued in Section 19.1, there can b
log211 bisections before a list is reduced to one-element lists, so we have O(nl) ste[
to perform 0 ( l o g n) many times; this makes quicksort take 0 (n ( l o g n) ) steps. G
uvertrge, although we saw that it can take quadratic steps in the worst (already sorted
case. '
The logarithmic behaviour here is characteristic of a 'divide and conquer' algorithm:
we split the problem into two smaller problems, solve these and thcn recombine the
reesults. The result is a comparatively efficient algorithm. which reaches its base caws
in 0 (log2 n) rather than 0 (nl) steps.
I The explamtion we have given here depend!, upon us rearranging the order of rhc c;~lculutionsteps; this
is Icgitimate if we observe that lazy cvaluation of combinalors is o p t i n i d , in the sense of taking fewest steps
to reach a rcsult: any rearrangement can only give more steps to our calculi~tion.so thc bound of n(log n)
holds.
The complexity of calculations 421
[ Exercises
19.1 1 Estimate the time complexity of the two reverse functions given here:
rev1 [I = [I
rev1 (x:xs) = revl xs ++ [XI
and
rev2 = shunt [I
shunt xs [I = 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
I (m 'mod' 2 == 0) = russ (n+n) (m 'div' 2)
I otherwise = russ (n+n) (m 'div' 2) + n
19.14 Show that thc worst-case time behaviour of the merge sort function bclow is
O(n(1og n)).
mSort xs
I (len < 2) = xs
1 otherwise = mer (mSort (take m xs)) (mSort (drop m xs))
where
len = length xs
m = len 'div' 2
empty = [I
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 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.
Exercises
19.15 Confirm the time conlplexities given in the table above for the two list imple-
mentations of sets.
19.16 Implement the operations subset, inter, makeset and mapset for the search
tree implementation, and estimate the time complexity of your implementations.
19.17 Give an implementation of sets as lists without repetitions, and estimate the time
complexity of the functions in your implementation.
Space behaviour 423
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 l , defined thus
[m .. nl
I n>=m = m: [ m + l .. nl
I otherwise = [I
Calculating [i .. nl gives
where we have underlined those parts of the result which can be output. To measure
the space complexity we look at the non-underlined part, which is of constant size. so
the space complexity i \ 0 (no). The calculation has approximately 2*n steps, giving it
linear time complexity, as expected.
The time taken to calculate this will be O(nl). and the space used will be O(nO),but
we will have to calculate the expression [I . . nl twice. Suppose instead that we
compute
exam2 = l i s t ++ l i s t
where
list=[1 . . nl
424 Time and space behaviour
The effect here is to compute the list [I . . nl once, so that we save its value after
calculating it in order to be able to use it again. Unfortunately, this means that after
evaluating l i s t , the whole of the list is stored, giving an O(nl) space complexity.
This is a general phenomenon. If we save something by referring to it in a where
clause we have to pay the penalty of the space that it occupies: if the space is available,
fair enough; if not, we have turned a working computation into one which fails for lack
of space.
This problem can be worse! Take the examples
exam3 = [ I . . n] ++ [ l a s t [I .. n]]
exam4 = l i s t ++ [last l i s t ]
where
l i s t =[ I . . n]
in which l a s t returns the last element of a non-empty list. The space required by
exam3 is 0 (no), while in exam4 it is 0 ( n l ) , since we hold on to the calculated valueof
l i s t even though we require only one value from it, the last. This feature, of keeping
hold of a large structure when we only need part of it, is called a dragging problem. In
the example here, the problem is clear, but in a larger system the source of a dragging
problem can be most difficult to find.
The lesson of these examples must be that while it is always sensible not to repeat
the calculation of a simple value, saving a compound value like a list or a tuple can
increase the space usage of a program.
Saving space?
As we saw in Section 19.2, the naive factorial function has 0 (nl) space complexity, as
it forms the expression
aFac 0 p = p
aFac n p = aFac (n-1) (p*n)
and compute the factorial of n using aFac n I . Now, we examine the calculation
newFac n
1-t aFac n I
aFac (n-I) ( l * n )
--
1-t
?? (n-1)==0 -.., F a l s e
aFac (n-2) (l*n*(n-I))
-- ...
aFac 0 (l*n* (n-1) * (11-21 *. . . *2*1)
(l*n* (n-l)* (n-2)*. . .*2*1)
Space behaviour 425
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 n~cclrcl,so
that they are calculated earlier. We do this here by adding a test; another method is
given in Section 19.5.
aFac n p
I p==p = aFac (n-I) (p*n)
-
aFac 4 1
aFac (4-1) (1*4)
?? (4-1)==0 1-* False
?? (1*4)==(1*4) ?.i True
?-t aFac (3-1) (4*3)
?? (3-1)==0 - False
- ?? (4*3)==(4*3) -.-i
aFac (2-1) (12*2)
True
--
?.i ...
aFac 0 (24*1)
(24*1)
^c-* 24
The lines (eqTest) show where the guard p==p is tested, and so where the intermediate
multiplications take place. From this we can conclude that this version has better
(constant) space behaviour.
Exercises
sumsquares : : I n t -> I n t
sumsquares n = sumList (map s q [ I . . n] )
where
Folding revisited
Strictness
A function is strict in an argument if the result is undetined 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
F a l s e && x = F a l s e
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.
f o l d r f st [I = st
f o l d r f s t (x:xs) = f x ( f o l d r f s t x s )
which we saw was of general application. Sorting a list, by insertion sort, was given by
iSort = foldr i n s [I
and indeed any primitive recursive definition over lists can be given by applying f oldr.
Writing the function applications as infix operations gives
f o l d r f st [a1 ,a 2 , . . . ,an-1,an]
A
.- a1 ' f ' (a2 ' f ' . . . ' f ' (a,-1 'f' (an ' f ' s t ) ) . . . I (f oldr)
and shows why the 'r' is added to the name: bracketing is to the right, with the starting
value s t appearing to the right of the elements also. I f f is lazy in its second argument,
we can see from (f o l d r ) that given the head of the list, output may be possible. For
instance, map can be defined thus
map f = f o l d r ( ( : ) . f ) [I
Folding revisited 427
As in Section 19.4, we see that the space complexity of this will be O(nO),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 o f f a c in Section 19.2 can be rewritten as
f a c n = f o l d r (*) 1 [ I .. nl
and we saw there that the effect was to give 0 (nl) space behaviour, since the multipli-
cations in equation (f o l d r ) 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
f o l d l (*) 1 [I . . n]
-A f o l d l (*) (1*1) [2 .. n]
-A ...
f o l d l (*) ( . . . ((1*1)*2)*. . .*n) [I
-- ( . . . ((1*1)*2)*. . .*n)
As in Section 19.2, the difficulty is that f o l d l as we have defined it is not strict in its
second argument. Using the standard function s e q
s e q : : a -> b -> b
-
f o l d l ' (*)
foldl'
1 [i .. n]
(*) 1 C2 . . n l
--
r~ foldl'
f oldl '
...
(*) 2 C3 . . n]
(*) 6 [4 . . n]
Clearly, this evaluation is in constant space, O(nO). Can we draw any conclusionsfrom
these examples'?
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 f o l d l ' . This covers the examples of (+I, (*) and so
forth.
We saw earlier that when map was defined using f o l d r we could begin to give output
before the whole of the list argument wasconstructed. If we use f o l d l ' instead, wewill
have to traverse the whole list before giving any output, since any f o l d l ' computation
follows the pattern
f o l d l ' f st1 x s l
-
A
. f o l d l ' f s t 2 xs2
-4 ...
-
--t
f o l d l ' f s t k XSk
...
f o l d l ' f stn [I
-A Stn
This version uses constant space, m d may not examine the whole list; f o l d r is therefore
the best choice.
Beside the examples of (+) and (*), there are many other examples where f o l d l '
is preferable. including:
Since f o l d l ' 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.
19.20 Define the functions to reverse a list and to convert a digit list into a number using
both f o l d r and f o l d l ' and compare their behaviour by means of calculation.
19.21 Is it better to define insertion sort using f o l d r or f o l d l ' ? Justify your answer.
19.22 How are the results of f o l d r and f o l d l ) related? You may like to use the
functions r e v e r s e and f l i p in framing your answer.
fibP 3
= (y,x+y>
where
(x,y) = f i b P 2
= (y1,x1+y1)
where
(xl,yl) = f i b P 1
= (~2,~2+~2)
where
(x2,y2) = f i b P 0
= (0,l)
= (1,l)
f i b s ! !O = 0
fibs! !1 = 1
f i b s ! !(n+2) = f i b s ! ! n + f i b s ! !(n+l)
This gives a description of the list, but it is not executable in this form. The first two
lines tell us that f i b s = 0 : 1 : r e s t , while the third equation tells us what the
r e s t is. The (n+2)nd element of f i b s is the nth element of r e s t ; similarly, the
( n + l ) s t element is the nth element of ( t a i l f i b s ) . We therefore have, for every n,
rest!!n = fibs!!n + (tail fibs)!!n
which says that each element is got by adding the corresponding elements of two lists,
that is
r e s t = z i p w i t h (+) f i b s ( t a i l f i b s )
a process network computing the Fibonacci numbers. This gives a linear time, constant
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 f ibP 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 subproblems will not
be independent, in general. A naive solution therefore will contain massive redundancy,
which we remove by building a tuhle of solutions to subproblems.
The example we consider is to find the length of a maximal common subsequence of
two lists -the subsequences need not have all their elements adjacent. In the examples
of
the length of 4 is given by the subsequence [ I , 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 gives
the basis of the Unix d i f f 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 19.3. 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 problematic
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,
432 Time and space behaviour
mLen x s [I = 0
mLen [I y s = 0
mLen (x:xs) (y:ys)
I x==y = 1 + mLen x s y s
I otherwise = max (mLen x s ( y : y s ) ) (mLen ( x : x s ) ys)
maxLen x s ys 0 j = 0 (maxLen.1)
maxLen x s ys i 0 = 0 (maxLen.2)
maxLen x s ys i j
I x s ! ! ( i - 1 ) == y s ! ! ( j - I ) = (maxLen x s y s (i-1) ( j - 1 ) ) + 1
(maxLen -3)
I otherwise = max (maxLen x s ys i ( j - 1 ) )
(maxLen x s ys (i-1) j )
(maxLen .4)
maxTab x s ys
= result
where
r e s u l t = [O,O . . 1 : z i p w i t h f [O . . 1 r e s u l t
f i prev
= ans
where
ans = 0 : z i p w i t h g [O . . 1 ans
g j v
I X S ! ! ==
~ ys!!j = prev! ! j + 1
I otherwise = max v (prev! ! ( j + l ) )
of course, is the source of the redundant computations - each of these may well give
rise to a computation of mLen x s ys. How are we to avoid this situation'? 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.
Avoiding recomputation: memoization 433
As an intermediate step. we rewrite the solution as maxLen which uses list indexing,
so that
SO,
r e s u l t = [0,0 .. 1 : ..
Theequations (maxLen. 2) to (maxLen. 4) tell us how todefine the IistmaxTab ! ! ( i + l )
from the list maxTab! ! i , and i, so we can define
maxTab x s y s = r e s u l t
where
result = [O,O .. ] : z i p w i t h f [O .. 1 result
where f : : I n t -> [ I n t ] -> [ I n t ] is the function taking i and the previous value,
maxTab! ! i, to maxTab! ! ( i + l ) . Now we have to define this latter, 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 + l ) ! ! j and j tomaxTab! ! ( i + l ) ! ! ( j + l ) , where wearealsoable touse
the values of maxTab! ! i, nanied by prev. Using these insights, the definition of g is
a straightforward transliteration of ( m a L e n . 3) and (maxLen. 4 ) :
arts = 0 : z i p w i t h g [O . . 1 a n s
g j v
I X S ! ! ==
~ ys! ! j = prev! ! j + 1
I otherwise = max v ( p r e v ! ! ( j + l ) )
Greedy algorithms
A greedy solution to a dynamic programming problem works by building up the optimal
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 listsina
single sweep, looking successively for the first points of agreement; we search all pairs
of indices smaller than n before looking at n. In an example, the greedy solution gives
which is not optimal: the subsequence [ I , 2,31 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
the line-splitting algorithm we gave in Chapter 7 is optimal in minimizing the sum
of the inter-word spaces when the lines are justified;
the Huffman codes described in Chapter 15 are optimal in the sense of giving the
shortest possible codings of files. We did not search all possible sets of codes in
giving the Huffman code, rather we built it up from locally sensible choices.
19.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
Cl ,2,31 and C2,4,1,2,31 above.
19.25 Can you give an improvement of the maximal common subsequence solution
along the lines o f f ibP, returning a complex (finite) data structure as the result
of a function call, rather than simply one value?
19.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 techniques
of this section, and also how you give a greedy solution to the problem. How do
the two solutions compare?
19.27 Based on the examples of this section, provide a program which gives the
difference between two files, matching the corresponding lines and giving the
output in a suitable form, such as a list of the pairs of matching line numbers or
a form copied from the Unix d i f f program.
Avoiding recomputation: memoization 435
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 straightforward
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 f old1 brings the space issue into focus, and the distinction 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 l a ~ yinfinile lists to memoizing
results for reuse; the transition from naive to efficient was done in a syctematic way.
which can be carried over to other application areas.
This chapter has provided an introduction to the study of functional program be-
haviour; much more information - particularly about functional data structures - can
be found in Okasaki (1998).
) ( Chapter 20 1
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 as.
pect of the language was introduced, and also by giving general advice on how to design
programs, in a distinct phase between giving a precise specification of the problem and
writing a solution in Haskell.
A functional programmer rnodels the real world at a high level of abstraction, concentrat-
ing on what relationships there are between values, embodied in function definitions.
This contrasts with a lower-level view in which the details of how items are related
predominate. For instance, in Haskell lists are simply values, whereas in C or Ctt
they become data structures built from pointers, and even in Java it is difficult to
present a suitably abstract model of lists. This higher-level approach has a numberof
consequences, which have come out in the course of the book.
Higher-order functions and polymorphism combine to support the construction of
general-purpose libraries of functions, such as the list functions in the Haskell
standard prelude and library. The map function, for instance,
map : : (a -> b) -> [a] -> [b]
embodies the 'pattern' of applying the same transformation l o every element in a list.
which will be reused in a host of applications of lists.
Also supporting reuse through overloading are type classes, used for instance in
giving the function
which tests for membership of a list using the overloaded equality function.
The definitions of functions are equations which express propertics of the functions
defined. From the definitions of map and function composition, ' . '. for example. it
is possible to prove that for all functions f and g,
Conclusion 437
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 d c w not aim to be
complcte, 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 report
(Peyton Jones and Hughes 1998), and the 'Gentle Introduction' of Hudak, Fasel and
Peterson (1997) also contains useful information about some of then], 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, h t t p : //www .h a s k e l l .org/
The text has discussed many of the most important functions in the standard pre-
lude but on thc whole has avoided discussing the contents of the libraries, which are
documented in Peyton Jones and Hughes (1998). These libraries fall into two classes.
First there are libraries of utilities, such as L i s t . h s which contains a multitude of
list-manipulating functions. These are in libraries, which can be included or not by the
programmer at will, so as not to clutter up the name space of the language.
Other libraries contain extensions of the language, including a library of arrays,
Array. hs, as well as facilities for file crcation and management, D i r e c t o r y . hs, and
for system links, System.hs. These libraries come with all Haskell implementations;
each implen~entationwill also come with particular extensions, usually available in the
form of library modules.
Haskcll was first defined in 1987, and has been modified and extended since then. This
t c ~ is
t written in Haskell 98, which is meant to provide a stable base system consisting
438 Conclusion
of tried and tested features. The progress of research in functional programming makes
it clear that a language like Haskell will not stand still forever and at the time of writing
there is an initiative under way to design Haskell 2, which will extend and modify the
language in a number of significant ways. Nevertheless, it is likely that systems will
continue to support the features of Haskell98 as outlined in this text. The Haskell home
page can be relied upon to contain up-to-date information on the status of Haskell.
Extending Haskell
to provide a language for describing graphical animations which interact with users
(Elliott and Hudak 1997),
There are now many resources on Haskell and functional programming to be found on
the World Wide Web. This text itself has a home page at
which lists all the links given here. The Haskell home page is at
h t t p ://www .haskell .org/
and information about the Haskell mailing list can also be found there.
The Haskell language was named in honour of Haskell Brooks Curry. A short
biography and photograph of Curry can be found at
For functional programming in general, the first place to start is the 'FAQ',
which gives details of all functional programming languages, as well as more general
information and indeed answers to frequently asked questions about the basics of
functional programming.
Information about a number of real-world applications of functional programming
can be found at
Functional programming languages are used in many universities and other institutions,
and resources on functional languages in education are accessible from
Chapter 17, and up to that point it looks at aspects of functional programming which
are broad1y shared with Standard ML (Milner et al. 1997; Appel 1993), the best known
and most widely used strict and strongly typed functional language, for which Paulson
(1996) provides an introduction. It is possible to model lazy evaluation within a strict
language, and Haskell provides facilities to make evaluation strict, so the two schools
are very close indeed.
A different style of functional programming, eschewing variables as much as possible,
was introduced in Backus (1978). Bird and de Moor (1997) is a recent text which
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 lanaguage, 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). An
imperative language with similiarities to LISP and used for telephone switching and
other real-time applications is Erlang (Armstrong, Virding and Williams 1993).
Two recent surveys of applications of functional programming languages in large-
scale projects are Runciman and Wakeling (1995) and Hartel and Plasmeijer (l995b),
and there is also information about this on the Web, as cited above.
In the last ten years, powerful techniques of implementation of especially lazy
functional languages have been developed. The twin texts (Peyton Jones 1987; Peyton
Jones and Lester 1992) describe these in lucid detail.
- it appears that practical languages can be defined. The advantage of languages like
these is that they make reasoning much more straightforward, as well as allowing a
programmer to express more of their intuitions about how a program behaves as a part
of the program. This text has already shown how a strongly typed language allows for
the capture of many errors at compile time, and strengthening the type system can only
help this.
A third issue is that of providing tool support for developers of functional programs.
As was evident in the discussion of lazy evaluation, it is often very difficult indeed to
predict the space behaviour of lazy programs; interesting work on this is reported in
Runciman and Riijemo ( 1 996).
These are only three of the possible directions for functional languages, and it is clear
that they provide a fertile approach to programming which will remain an important
element of computing science in years to come.
Functional, imperative
and 00 programming
Consider the example offinding the sum of squares of natural numbers up to a particular
number. A functional program describes the values that are to be calculated, directly.
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-7,ero n is
got by finding the sum to n-l 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+l ;
s := i*i + s ;
end {while)
The sum is the final value of the variable s,which is changed repeatedly during program
execution, as is the 'count' variable, i. The effect of the program can only be seen
by following the sequence of changes made to these variables by the commands in the
program, while the functional program can be read as a series of equations defining
Functional, imperative and 00 programming 443
the sum of squares. This meaning is explicit in the functional program, whereas the
imperative program has an overall effect which is not obvious from the program itself.
A more striking algorithm still is one which is completely explicit: 'to find the sum of
squares, build the list of numbers 1to n, square each of them, and sum the result'. This
program, which uses neither complex control flow, as does the imperative example, nor
recursion as seen in the function sumsquares, can be written in a functional style, thus:
newSumSq : : I n t -> I n t
newSumSq n = sum (map square [I . . nl )
where square x = x*x, the operation map applies its first argument to every member
of a list, and sum finds the sum of a list of numbers. More examples of this sort of
data-directed programming can be seen in the body of the text.
An important difference between the two styles is what is meant by yome of the
terminology. Both 'function' and 'variable' have different interpretations.
As was explained earlier, a function in a functional program is simply something
which returns a value which depends upon some inputs. In imperative and object-
oriented languages like Pascal, C, C++ and Java a function is rather different. It will
return a value depending upon its arguments, but in general it will also change the
values of variables. Rather than being a pure function it is really a procedure which
returns a value when it terminates.
In a functional program a variable stands for an arbitrary or unknown value. Evcry
occurrence of a variable in an equation is interpreted in the same way. They are just
like variables in logical formulas, or the mathematical variables familiar from equations
like
a2 - b2 = (a-b) (a+b)
In any particular case, the value of d l three occurrences of a will be the same. In exactly
the same way, in
Program verification
Probably the most important difference between functional and imperative programs
is logical. As well as being a program, a functional definition is a logical equation
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>O,it is the case that
To start with,
Now. n*n is positive, and if sumsquares (n-1) is positive, their sum, sumsquares
n, must be. This proof can he 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 effect
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 transformation in which
programs are transformed to other programs which have the same effect but better
performance, for example. Again, this is difficult for traditional imperative languages.
and so on. If per : : Person then name per : : String,similarly to r.name being
a string variable if r is a variable of type Person in Pascal.
Haskell 98 also contains records with named fields, rather more like those of Pascal.
For further details, see the Haskell Report (Peyton Jones and Hughes 1998).
then we have the following correspondence, where the Haskell head and tail functions
give the head and tail of a list.
[1 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
function sumList(xs:list):integer;
446 Functional, imperative and 00 programming
begin
if xs=nil
then sumList : = 0
else sumList := xsA.head + sumList(xs-.tail)
end ;
A second example is
Higher-order functions
Traditional imperative languages give little scope for higher-order programming; Pas-
cal, 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 t t
it is possible to return objects which represent functions by overloading the function
Functional, imperative and 00 programming 447
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 func-
tions, as they take commands, cl, c2 etc. into other commands,
if b then c l else c 2 while b do c l
just as map takes one function to another. Turning the analogy around, we can think of
higher-order functions in Haskell as control structures which we can detine ourselves.
This perhaps explains why we form libraries of polymorphic functions: they are the
control structures we use in programming particular sorts of system. Examples in the
text include libraries for building parsers (Section 17.5) and interactive 110 programs
(Chapter 18), as well as the built-in list-processing functions.
Polymorphism
Again, this aspect is poorly represented in many imperative languages; the best we can
do in P a s c a l , say, is to use a text editor to copy and modify the list processing 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 implement
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 completely
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 reusahl~in Haskell; it remains to be seen whether this will also be true
of advanced imperative languages.
The abstract data types, introduced in Chapter 16, are very like the abstract data
types of Modula-2 and so on; the design methods we suggest for use of abstract data
types mirror aspects of the object-based approach advocated for modern imperative
languages such as Ada.
The Haskell class system also has object-oriented aspects, as we saw in Section 14.6.
It is important to note that Haskell classes are in some ways quite different from the
classes of, for instance, C++. In Haskell classes are made up of types, which themselves
have members; in C++ a class is like a type, in that it contains ob.jects. Because of this
many of the aspects of object-oriented design in C++ are seen as issues of type design
in Haskell.
List comprehensions
List comprehensions provide a convenient notation for iteration along lists: the ana-
logue 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 forb 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:=O to xLen-I do
for j :=O to yLen-1 do
write( x[il+y[jl )
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
[O .. length xs - I]
and so a Haskell analogue of (twoFor) can be written thus:
[ xs!!i + ys!!j I i <- [O . . length xs - 11 ,
j <- [O . . length ys - 11 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 : integer1:integer;
begin
y := y+l;
succ := x+l
end ;
Functional, imperative and 00 programming 449
This function adds one to its argument, but also has the side-effect of increasing y by
one. If we evaluate f (y ,s u c c ( z ) ) we cannot predict the effect it will have.
I f f evaluates its second argument first, y will be increased before being passed to f ;
on the other hand, i f 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 programs.
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.
Section 17.6 introduced infinite lists, and one of the first examples given there was an
infinite list of random numbers. This list could be supplied to a function requiring 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 var-iahle will ttrke
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.8 that there has been recent important work on integrating
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 features
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 prograln-
ming paradigms without compromising the purity of the language. Other languages.
including Standard ML (Milner, Tofte and Harper 1990), 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 programming
languages in the design of Java extensions. One of the main drawbacks of Java is that it
lacks a generic mechanism; the Pizza language (Odersky and Wadler 1997) adds this,
together with Haskell-style pattern matching, and Pizza is a forerunner of the Generic
Java extension, GJ, www. c s .bell-labs .com/who/wadler/pizza/gj/.
( Appendix
) B )
Glossary
We include this glossary to give a quick reference to the most widely used terminology 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.
Abstract type An abstract type Arguments are also known as inputs and
definition consists of the type name, the parameters.
signature of the type, and the
Associativity The way in which an
implementation equations for the names
expression involving two applications of
in the signature.
an operator is interpreted. If x#y#z is
Algebraic type An algebraic type interpreted as (x#y)#z then # is left
definition states what are the associative, if as x#(y#z) it is right
constructors of the type. For instance, associative; if both bracketings give the
the declaration same result then # is called associative.
values are needed will be evaluated, and without any action, and sequence two
moreover, only the parts of structures monadic operations.
which are needed will be examined.
Monomorphic A type is
Linear complexity Order 1, 0 (nl 1, 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 the
some order, potentially containing a expression is an instance o f t .
particular item more than once. The list
.
[2, I , 3 , 2 3 is of type [ I n t l for Mutual recursion Two definitions,
each of which depends upon the other.
example.
Literal Something that is 'literally' a Name A definition associates a name or
valuc: it needs no evaluation. Examples identifier with a value. Names of classes.
include 34, [23] and " s t r i n g " . constructors and types must begin with
capital letters; names of values,
Local definitions The definitions variables and type variables begin with
appearing i n a where clause or a l e t
small letters. After the first letter, any
expression. Their scope is the equation or letter, digit, ' ' ' or T can be used.
expression to which the clause or l e t 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
Mathematical induction A method of of a part of a definition is expressed using
proof for statements of the form 'for all the l q o u t of a script, rather than an
natural numbers n, the statement P(n) explicit symbol for the end.
holds'. Operation Another name for function.
The proof is in two parts: the base
case, at zero, and the induction step, at Operator A function which is written
which P(n) is proved on the assumption i n infix form, between its arguments.
that P (n-I) holds. The function f is made infix thus: 'f '.
String The type String is a synonym list and function types. New types can
for lists of characters, [Char]. be defined using the algebraic and
Structural induction A method of abstract type mechanisms, and types can
proof for statements of the form 'for all be named using the type synonym
finite lists xs, the statement P(xs) holds mechanism.
of xs'. The proof is in two parts: the base Type variable A variable which
case, at [I, and the induction step, at appears in a polymorphic type. An
which P(y:ys) is provedon the identifier beginning with a small letter
assumption that P (ys) holds. can be used as a type variable; in this text
Also used of the related principle for any we use the letters at the start of the
algebraic type. alphabet, a, b, c and so on.
Substitution The replacement of a
variable by an expression. For example, Undefinedness The result of an
(9+12) is given by substituting 12 for n expression whose evaluation continues
in (9+n). Types can also be substituted forever, rather than giving a dejned
for type variables; see the entry for result.
instance. Unification The process of finding a
Synonym Naming a type is called a common instance of two (type)
type synonym. The keyword type is expressions containing (type) variables.
used for synonyms.
Value A value is a member of some
Syntax The description of the properly type; the value of an expression is the
formed programs (or sentences) of a result of evaluating the expression.
language.
Variable A variable stands for an
Transformation Turning one program
arbitrury value, or in the case of type
into another program which computes
variables, an arbitrary type. Variables and
identical results, but with different
type variables have the same syntax as
behaviour in other respects such as time
names.
or space efficiency.
Tuples A tuple type is built up from a Verification Proving that a function or
number of component types. Elements of functions have particular logical
the type consist of tuples of elements of properties.
the component types, so that
Where clause Definitions local to a
(conditional) equation.
for instance. Wild card The name for the pattern '_',
Type A collection of values. Types can which is matched by any value of the
be built from the base types using tuple, appropriate type.
HaskeII operators
The Haskell operators are listed below in decreasing order of binding power: see Section
3.7 for a discussion of associativity and binding power.
8 **,
7 *, /, 'div',
'mod', 'rem',
'quot (
6 +, - :+
5 \\
4 /=, < <= ==
9 I ,
The restrictions on names of operators, which are formed using the characters
are that operators must not start with a colon; this character starts an infix constructor.
The operators - and ! can be user-defined, but note that they have a special meaning
in certain circumstances -the obvious advice here is not to use them. Finally, certain
.
combinations of symbols are reserved, and cannot be used: . : : => = Q \ I <-
->.
458 Haskell operators
which states that &&& has binding power 7, and is a left associative operator. We
can also declare operators as non-associative ( i n f i x ) and right associative (inf ixr).
Omitting the binding power gives a default of 9. These declarations can also be used
for back-quoted function names, as in
i n f i x 0 'poodle'
( Appendix D )
Understanding
programs
This appendix is included to offer help to readers confronted with a n unfamiliar function
definition. There are various things we can do with the definition, and these are examined
in turn here. Given a functional program like
mapwhile f p [I = [I
mapwhile f p (x:xs)
I P x = f x : mapwhile f p xs
1 otherwise = [I
we can understand what it means in various complementary ways. We can read the
program itself, we can write calculations of examples using the program, we can prove
progertiPs~ofthe_p~oq-amL and we can estimate its space and time complexity,
- - - - -
- - - - - - - - - - - - - - - - - - - -
Besides any comments which might accompany a program, the program itself is its
most important documentation.
The type declaration gives information about the input and output types: formapwhile,
we have to supply three arguments:
The function definition itself is used to give values of mapwhile, but also can be read
directly as a description of the program.
In the definition we have a complete description of how the program behaves. but we
can animate this by trying specific examples.
A more concrete view of what the progritnl does is given by calculating particular
examples. For instance,
(Int -> Int) -> (Int -> Bool) -> [ ~ n t l-> [Intl
of its polymorphic type, given by replacing the type variables a and b by the type I n t .
We can get a deeper understanding about a program by proving properties that the
program might have. For mapwhile, we might prove that for all f , p and finite lists xs.
Program behaviour
It i \ not hard to see that the program will at worst take time linear (that is 0 ( n l ) ) i n the
length (n) of the list argument assuming 0 (no) behaviour o f f and p. as it runs through
the elements of the list once, if at all.
The space behaviour is more interesting; becau\e we can output the head of a list
once produced. the space required will be constant, a\ suggested by underlining thc
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 delinition we can begin to understand what its cffect
is by calculating various s~nallexamples. If we are given a collection of function\, we
can test out the functions from the bottom up, building one calculation on top of anotlier.
The important thing is to realize that rather than being ,tuck, we can get started by
calculating representative examples to show us the way.
( Appendix E )
Implementations of
Implementations of Haskell have been built at various sites around the world. This text
discusses the Hugs interpreter, which was developed in a joint effort by staff at the
Universities of Nottingham in the UK and Yale in the USA. Compilers have been developed
at the University of Glasgow, UK, and Chalmers Technical University, Goteborg, Sweden.
Hugs
For the Unix version of Hugs you should follow the installation notes.
/'
Note: Downloading Hugs for Windows
If you want to download the standard installation of Hugs which will set up the
appropriate registry entries, you should download one of the
selfinstall.exe
self install.zip
tiles which will run an InstallShield script to make the appropriate settings and so
on. If you download the binaries, you will have to make these settings and so
forth for yourself.
You can choose to set the default editor for Hugs. The most straightforward way of
doing this under Windows is to run WinHugs and to change the settings there. These
changes persist to future invocations of both Hugs and WinHugs. The Programmer's
File Editor is a freely available editor for Windows systems:
Implementationsof Haskell 463
For the Macintosh, there is a port of Hugs 1.4 to the Power Macintosh OS at
http://www.dcs.gla.ac.uk/fp/software/ghc/
http://www.cs.chalmers.se/"augustss/hbc/hbc.html
http://www.cs.york.ac.uk/fp/nhcl3/
Further information
Up-to-date information about future developments of these and any other implementa-
tions will be available from the Haskell home page,
Hugs errors
This appendix examines some of the more common programming errors in Haskell, and
shows the error messages to which they give rise in Hugs.
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 warning
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 Hugs; we have chosen the messages which are both common and require some
explanation; messages like
Program e r r o r : (head [I 1
are self-explanatory. The messages are classified into roughly distinct areas. Syntax errors
show up malformed programs, while type errors show well-formed programs in which
objects are used at the wrong types. In fact, an ill-formed expression can often show
itself as a type error and not as a syntax error, so the boundaries are not clear.
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. Typing
'2==3)' will provoke the error message
ERROR: Syntax error in input (unexpected ' ) I )
The syntax of patterns is more restricted than the full expression syntax, and so we get
error messages like
Repeated v a r i a b l e "x" i n p a t t e r n
when we use the same variable more than once within a pattern.
In specifying constants, we can make errors: floating-point numbers can be too large,
and characters specified by an out-of-range ASCII code:
Inf . 0
ERROR: Decimal c h a r a c t e r escape out of range
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 keywords are
c a s e c l a s s d a t a d e f a u l t d e r i v i n g do e l s e i f import i n i n f i x
i n f i x 1 i n f i x r i n s t a n c e l e t module newtype of t h e n t y p e where
Undefined c o n s t r u c t o r f u n c t i o n "Montana"
Type errors
As we have seen in the body of the text, the main type error we meet is exemplified by
the response to typing ' c J && True to the Hugs prompt:
which is provoked by using a Char where an Bool is expected. Other type errors, such
as
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
a b s -2 instead of a b s (-2) gives the error message
because it is parsed as 2 subtracted from a b s : :a->a, and the operator '-' expects
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++ [21 and [21 : [21.
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.
mycheck : : I n t -> Bool
mycheck n = o r d n == 6
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 [I = 45
which shows the point at which evaluation can go no further, since there is no case
in the definition of b a t to cover a non-empty list. Similar errors come from built-in
functions, such as head.
Other errorc happen because an arithmetical constraint has been broken. These
include an out-of-range list index, division by Lero, 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:
Program e r r o r : P r e l u d e L i s t . ! ! : index t o o l a r g e
Program e r r o r : {primDivInt 3 0 )
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 message\ for these and other
errors are self-explanatory.
System messages
In response to some commands and interrupts, the system generates messages. including
which shows that the space consumption of the evaluation exceeds that available. One
e the heap. To see the current s i ~ oe f thc heap
way around this is to increase the s i ~ of
and the other settings of the system type
468 Hugs errors
The message given there shows how the heap size can be changed, as well as how to
affect other system parameters.
If the option +s is set, the system prints diagnostic information of the form
( 2 reductions, 8 cells)
The number of reduct ions corresponds to the number of steps in our calculations and
the cells to the total space usage.
A measure of the space complexity of a function, as described in Chapter 19, is given
by the size of the smallest heap in which the evaluation can take place; there is no direct
measure of this given by the system.
Bibliography
Abelson, H., G. J. Sus\man and J. Sussman (1996). The Structure urzd Interpretation
of Computer Programs (2nd edn). MIT Press.
3.
Appel, A. ( 1993). A critique of Standard ML. Journcrl ofFunctionu1 Progr~~nlming,
Armstrong, J., R. Virding and M. Williams (1993). Concurrmt Programrning in
ERLANG. Prentice-Hall.
Augustsson, L. (1 998). Cayenne - a language with dependent types. See Hudak and
Queinnec ( 1998).
Backus, J. (1978). Can programming be liberated from the Von Neumann style?
Comrnunications of the ACM, 21(8).
Bird, R. and 0. de Moor (1997). Algebra of Programnzing. Prentice-Hall.
Cormen, T. H., C. E. Leiserson and R. L. Kivest (1990). Introduction to A1gor.irbm.s.
MIT Press.
Elliott, C. and P. Hudak (1997). Functional reactive animation. In Proceedings of
the 1997 ACM SIGPLAN International Conference on Functional Progrcirnrning
(ICFP97). ACM Press.
Finne, S., D. Leijen, E. Meijer and S. Peyton Jones (1998). Hldirect: A binary foreign
language interface for Haskell. See Hudak and Queinnec ( 1 998).
Gordon, A. J. ( 1994). Functiorznl Programming and Input/Outpuf. British Computer
Society Distinguished Dissertations in Computer Science. Cambridge University
Press.
Peyton Jones, S. and J. Hughes (eds) (1998). Standard Lihraries.fi)r the Hciskell 98
Programming Language. h t t p ://www .haskell . org/library/.
Hartel, P. and R. Plasmeijer (eds) (199%). Functional Programming Languc~g~s in
E,~dcrcotion(FPLE). Springer-Verlag, Lecture Notes in Computer Science. 1022.
470 Bibliography
! ,457
! !,91,457
<<, 415
0,386
(. .), 282
*, 7,36,44, 224,457
**, 224,457
+I n t ' 228
0,414
0,414
.,
+, 3, 5, 36,44, 224, 343, 457
++, 14, 85, 88, 91, 93, 121, 144, 146,
&&&, 51
143
V, 150
+, 140
=>, 212
>*>, 357,358,457
>.>, 170,457
>>=, 400,402,4 10,457
>@>,403
\, 173
-,36, 44, 457
--,457
, 116
I-, 21
\\, 347,457
1 1,33,457
Index 473
f i b P a i r , 76 f reqMerge. 294
f i b s , 43 1 frequency, 293,294
f ibStep, 76 Frequency. l h s , 293
f ibTwoStep, 76 f romEnum, 22 1
Figure. 270 f romInt, 44.45.224
f i l l . 132 f romInteger, 224
filter, 153,452 f r o n t s e q , 208
f i l t e r , 158, 196 f st. 75,89
type of, 158 function, 3, 18, 443,452
f i l t e r s e t , 324 anonymous, 174
:f i n d , 24 as result, 17 1
finiteness, 138, 139 binary, 5 1
first-class citizens, functions, 14 box diagram, 3, 33
f i r s t D i g i t , 1 18 curried, see currying
F i r s t L i t e r a t e . l h s , 21 definition, 9
F i r s t s c r i p t . hs, 20 definition of polymorphic. 88
fixity, 50 described by expression, 172
f l i p , 186 first-class citizens, 14
f l i p H , 13,97. 168, 181, 198 general, 152
f l i p V , 4, 5, 7, 13, 97, 156, 177, 181, higher-order, 155, I67
198 infix, 155,453
Flo, 304 infix version of, 36
F l o a t , 32,43,223,213 inverse, 186. 194
F l o a t i n g , 224 l u y , 426
floating point operators, 44, 457 number of arguments, 179
floating-point operators, 224 overloaded, see overloading
folding, 154, 161.426-429,452 prefix, 155, 455
and primitive recursion, 155, 16 1- representmg properties, 157
I64 selector, 75
designing folds, 428 strict, 426
f o l d l ' ,428 function application, 3, 7,. I, 17 1,450
type of, 428 partial, secJ partial application
f o l d l , 427 syntax, 178
type of, 427 function composition, 1 I, 169- 17 1 , 187,
f o l d r , 161-163, 195,235,426 195.232,45 1
type of, 162,235 as pipeline, 19 1
f o l d r l , 161 associative, 194
type of, 16 1 forward, 169
f o l d s e t , 324 pitfalls, 170
formal parameter, 10 type of, 169
f o r m a t B i l l , 110, 1 1 1 function definition
forward composition, 169, 452 as description, 137
fp-list, 377 general case
F r a c t i o n a l , 224 single equation, 10
Frank, 159 general form, 39, 106
free variable, 140 layout, see layout
478 Index
simulation, 267, 309-3 15, 373-375 structural induction, 141 , 196, 197,377.
experimenting, 375 456
the queue, 3 1 1 Maybe type, 276
the server, 3 12 expression type, 277
simulationInput,374 for lists, 14 1
simulationstep,3 13 trees, 274
sin,224 Sub, 250
sing,323 subseq,207
size, 309,319 subset,324
snd, 75 substitution, 338, 345. 456
snoc, 163 duplication on, 339
software, 2 succ,221
sorting, 123 succeed, 356,357
insertion, 123, 155 sum,92, 1 19, 142, 154, 195,377
merge sort, 293 sum type, 258
quicksort, 127, 189,420 sumFacs,63
sortls, 189 sumFun,63
specialization, 177 sumInts,394
of polymorphic function, 22 1 sumsquares, 103
specification, 67 sumTree,252,407
splitAt,91 superimpose,4,99, I82
splitlines, 1.2, 155 symbolic evaluation, 137
splitwords, 130 syntax, 30,46-5 1,456
spot, 356,357 of application, 178
sqrt,44,224 syntax error, 30,464
square, 9 system messages, 467
squashMaybe,264 System.hs, 437
standard libraries, 26
Table,287,410
location of, 27
tail,91,261
Standard ML, 440
take, 91, 126, 165
standard prelude, see Prelude.hs
takewhile, 166
state, 442,449
tan, 224
State, 409,410
tclosure, 328-330
store. 300 Temp,243
as n ADT, 302 termination, 66, 138, 253
as a list. 302 testing, 16, 67-69
as function, 303 and proof, 137
Store, 300.303,354,396 black box, 67
stream, 455 black-box, 67
Stree,319 library database. 86
sTree,407 special cases, 67
strict,427 testing groups, 67
String,73,7X, 92 white box, 67, 69
String.hs, 93 text processing, 128,434
strings, 92-95,456 filling, 128
486 Index
xs, 118
ys, 118