100% found this document useful (1 vote)
137 views36 pages

Full Download Algorithm Design With Haskell Richard S. Bird PDF

Algorithm

Uploaded by

cealangwgw
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
137 views36 pages

Full Download Algorithm Design With Haskell Richard S. Bird PDF

Algorithm

Uploaded by

cealangwgw
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 36

Experience Seamless Full Ebook Downloads for Every Genre at textbookfull.

com

Algorithm Design with Haskell Richard S. Bird

https://textbookfull.com/product/algorithm-design-with-
haskell-richard-s-bird/

OR CLICK BUTTON

DOWNLOAD NOW

Explore and download more ebook at https://textbookfull.com


Recommended digital products (PDF, EPUB, MOBI) that
you can download immediately if you are interested.

Thinking Functionally With Haskell Richard Bird

https://textbookfull.com/product/thinking-functionally-with-haskell-
richard-bird/

textboxfull.com

The Algorithm Design Manual 3rd Edition Steven S. Skiena

https://textbookfull.com/product/the-algorithm-design-manual-3rd-
edition-steven-s-skiena/

textboxfull.com

Bird s engineering mathematics 9th Edition John Bird

https://textbookfull.com/product/bird-s-engineering-mathematics-9th-
edition-john-bird/

textboxfull.com

Bird s Higher Engineering Mathematics 9th Edition John


Bird

https://textbookfull.com/product/bird-s-higher-engineering-
mathematics-9th-edition-john-bird/

textboxfull.com
The Ethical Algorithm The Science of Socially Aware
Algorithm Design 6th Edition Kearns

https://textbookfull.com/product/the-ethical-algorithm-the-science-of-
socially-aware-algorithm-design-6th-edition-kearns/

textboxfull.com

Bird s basic engineering mathematics 8th Edition John O


Bird

https://textbookfull.com/product/bird-s-basic-engineering-
mathematics-8th-edition-john-o-bird/

textboxfull.com

Haskell The Ultimate Beginner s Guide to Learn Haskell


Programming Step by Step 1st Edition Claudia Alves

https://textbookfull.com/product/haskell-the-ultimate-beginner-s-
guide-to-learn-haskell-programming-step-by-step-1st-edition-claudia-
alves/
textboxfull.com

Thinking with Types Type level Programming in Haskell


Sandy Maguire

https://textbookfull.com/product/thinking-with-types-type-level-
programming-in-haskell-sandy-maguire/

textboxfull.com

Applied Computational Thinking with Python Algorithm


design for complex real world problems 2nd Edition Sofía
De Jesús | Dayrene Martinez
https://textbookfull.com/product/applied-computational-thinking-with-
python-algorithm-design-for-complex-real-world-problems-2nd-edition-
sofia-de-jesus-dayrene-martinez/
textboxfull.com
ALGORITHM DESIGN WITH HASKELL

This book is devoted to five main principles of algorithm design: divide and
conquer, greedy algorithms, thinning, dynamic programming, and exhaustive
search. These principles are presented using Haskell, a purely functional language,
leading to simpler explanations and shorter programs than would be obtained
with imperative languages. Carefully selected examples, both new and standard,
reveal the commonalities and highlight the differences between algorithms. The
algorithm developments use equational reasoning where applicable, clarifying the
applicability conditions and correctness arguments. Every chapter concludes with
exercises (nearly 300 in total), each with complete answers, allowing the reader to
consolidate their understanding and apply the techniques to a range of problems.
The book serves students (both undergraduate and postgraduate), researchers,
teachers, and professionals who want to know more about what goes into a good
algorithm and how such algorithms can be expressed in purely functional terms.
A L G O R I T H M D E S I G N W I T H HASKELL

RICHARD BIRD
University of Oxford

JEREMY GIBBONS
University of Oxford
University Printing House, Cambridge CB2 8BS, United Kingdom
One Liberty Plaza, 20th Floor, New York, NY 10006, USA
477 Williamstown Road, Port Melbourne, VIC 3207, Australia
314–321, 3rd Floor, Plot 3, Splendor Forum, Jasola District Centre, New Delhi – 110025, India
79 Anson Road, #06–04/06, Singapore 079906

Cambridge University Press is part of the University of Cambridge.


It furthers the University’s mission by disseminating knowledge in the pursuit of
education, learning, and research at the highest international levels of excellence.

www.cambridge.org
Information on this title: www.cambridge.org/9781108491617
DOI: 10.1017/9781108869041
© Richard Bird and Jeremy Gibbons 2020
This publication is in copyright. Subject to statutory exception
and to the provisions of relevant collective licensing agreements,
no reproduction of any part may take place without the written
permission of Cambridge University Press.
First published 2020
Printed in the United Kingdom by TJ International Ltd, Padstow Cornwall
A catalogue record for this publication is available from the British Library.
ISBN 978-1-108-49161-7 Hardback
Cambridge University Press has no responsibility for the persistence or accuracy of
URLs for external or third-party internet websites referred to in this publication
and does not guarantee that any content on such websites is, or will remain,
accurate or appropriate.
For Stephen Gill (RB) and Sue Gibbons (JG).
Contents

Preface page xiii

PART ONE BASICS 1


1 Functional programming 5
1.1 Basic types and functions 5
1.2 Processing lists 7
1.3 Inductive and recursive definitions 9
1.4 Fusion 11
1.5 Accumulating and tupling 14
1.6 Chapter notes 16
References 16
Exercises 16
Answers 19
2 Timing 25
2.1 Asymptotic notation 25
2.2 Estimating running times 27
2.3 Running times in context 32
2.4 Amortised running times 34
2.5 Chapter notes 38
References 38
Exercises 38
Answers 40
3 Useful data structures 43
3.1 Symmetric lists 43
viii Contents

3.2 Random-access lists 47


3.3 Arrays 51
3.4 Chapter notes 53
References 54
Exercises 54
Answers 56

PART TWO DIVIDE AND CONQUER 59


4 Binary search 63
4.1 A one-dimensional search problem 63
4.2 A two-dimensional search problem 67
4.3 Binary search trees 73
4.4 Dynamic sets 81
4.5 Chapter notes 84
References 84
Exercises 85
Answers 87
5 Sorting 93
5.1 Quicksort 94
5.2 Mergesort 96
5.3 Heapsort 101
5.4 Bucketsort and Radixsort 102
5.5 Sorting sums 106
5.6 Chapter notes 110
References 110
Exercises 111
Answers 114
6 Selection 121
6.1 Minimum and maximum 121
6.2 Selection from one set 124
6.3 Selection from two sets 128
6.4 Selection from the complement of a set 132
6.5 Chapter notes 135
References 135
Exercises 135
Answers 137
Contents ix

PART THREE GREEDY ALGORITHMS 141


7 Greedy algorithms on lists 145
7.1 A generic greedy algorithm 145
7.2 Greedy sorting algorithms 147
7.3 Coin-changing 151
7.4 Decimal fractions in TEX 156
7.5 Nondeterministic functions and refinement 161
7.6 Summary 165
7.7 Chapter notes 165
References 166
Exercises 166
Answers 170
8 Greedy algorithms on trees 177
8.1 Minimum-height trees 177
8.2 Huffman coding trees 187
8.3 Priority queues 196
8.4 Chapter notes 199
References 199
Exercises 199
Answers 201
9 Greedy algorithms on graphs 205
9.1 Graphs and spanning trees 205
9.2 Kruskal’s algorithm 208
9.3 Disjoint sets and the union–find algorithm 211
9.4 Prim’s algorithm 215
9.5 Single-source shortest paths 219
9.6 Dijkstra’s algorithm 220
9.7 The jogger’s problem 224
9.8 Chapter notes 228
References 228
Exercises 229
Answers 231

PART FOUR THINNING ALGORITHMS 237


10 Introduction to thinning 241
10.1 Theory 241
10.2 Paths in a layered network 244
10.3 Coin-changing revisited 248
x Contents

10.4 The knapsack problem 252


10.5 A general thinning algorithm 255
10.6 Chapter notes 257
References 257
Exercises 257
Answers 261
11 Segments and subsequences 267
11.1 The longest upsequence 267
11.2 The longest common subsequence 270
11.3 A short segment with maximum sum 274
11.4 Chapter notes 280
References 281
Exercises 281
Answers 283
12 Partitions 289
12.1 Ways of generating partitions 289
12.2 Managing two bank accounts 291
12.3 The paragraph problem 294
12.4 Chapter notes 299
References 300
Exercises 300
Answers 303

PART FIVE DYNAMIC PROGRAMMING 309


13 Efficient recursions 313
13.1 Two numeric examples 313
13.2 Knapsack revisited 316
13.3 Minimum-cost edit sequences 319
13.4 Longest common subsequence revisited 322
13.5 The shuttle-bus problem 323
13.6 Chapter notes 326
References 326
Exercises 327
Answers 330
14 Optimum bracketing 335
14.1 A cubic-time algorithm 336
14.2 A quadratic-time algorithm 339
14.3 Examples 341
Contents xi

14.4 Proof of monotonicity 345


14.5 Optimum binary search trees 347
14.6 The Garsia–Wachs algorithm 349
14.7 Chapter notes 358
References 358
Exercises 359
Answers 362

PART SIX EXHAUSTIVE SEARCH 365


15 Ways of searching 369
15.1 Implicit search and the n-queens problem 369
15.2 Expressions with a given sum 376
15.3 Depth-first and breadth-first search 378
15.4 Lunar Landing 383
15.5 Forward planning 386
15.6 Rush Hour 389
15.7 Chapter notes 393
References 394
Exercises 395
Answers 398
16 Heuristic search 405
16.1 Searching with an optimistic heuristic 406
16.2 Searching with a monotonic heuristic 411
16.3 Navigating a warehouse 415
16.4 The 8-puzzle 419
16.5 Chapter notes 425
References 425
Exercises 426
Answers 428

Index 432
Preface

Our aim in this book is to provide an introduction to the principles of algorithm


design using a purely functional approach. Our language of choice is Haskell and all
the algorithms we design will be expressed as Haskell functions. Haskell has many
features for structuring function definitions, but we will use only a small subset of
them.
Using functions, rather than loops and assignment statements, to express algo-
rithms changes everything. First of all, an algorithm expressed as a function is
composed of other, more basic functions that can be studied separately and reused
in other algorithms. For instance, a sorting algorithm may be specified in terms of
building a tree of some kind and then flattening it in some way. Functions that build
trees can be studied separately from functions that consume trees. Furthermore, the
properties of each of these basic functions and their relationship to others can be
captured with simple equational properties. As a result, one can talk and reason
about the ‘deep’ structure of an algorithm in a way that is not easily possible with
imperative code. To be sure, one can reason formally about imperative programs by
formulating their specifications in the predicate calculus, and using loop invariants
to prove they are correct. But, and this is the nub, one cannot easily reason about
the properties of an imperative program directly in terms of the language of its
code. Consequently, books on formal program design have a quite different tone
from those on algorithm design: they demand fluency in both the predicate calculus
and the necessary imperative dictions. In contrast, many texts on algorithm design
traditionally present algorithms with a step-by-step commentary, and use informally
stated loop invariants to help one understand why the algorithm is correct.
With a functional approach there are no longer two separate languages to think
about, and one can happily calculate better versions of algorithms, or parts of
algorithms, by the straightforward process of equational reasoning. That, perhaps, is
the main contribution of this book. Although it contains a fair amount of equational
reasoning, we have tried to maintain a light touch. The plain fact of the matter is
xiv Preface

that calculation is fun to do but boring to read – well, too much of it is. Although it
does not matter very much whether imperative algorithms are expressed in C or Java
or pseudo-code, the situation changes completely when algorithms are expressed
functionally.
Many of the problems considered in this book, especially in the later parts, begin
with a specification of the task in hand, expressed as a composition of standard
functions such as maps, filters, and folds, as well as other functions such as perms for
computing all the permutations of a list, parts for computing all the partitions, and
mktrees for building all the trees of a particular kind. These component functions
are then combined, or fused, in various ways to construct a final algorithm with the
required time complexity. A final sorting algorithm may not refer to the underlying
tree, but the tree is still there in the structure of the algorithm. The notion of fusion
dominates the technical and mathematical aspects of the design process and is really
the driving force of the book.
The disadvantage for any author of taking a functional approach is that, be-
cause functional languages such as Haskell are not so well known as mainstream
procedural languages, one has to spend some time explaining them. That would
add substantially to the length of the book. The simple solution to this problem
is just to assume the necessary knowledge. There is a growing range of textbooks
on languages like Haskell, including our own Thinking Functionally with Haskell
(Cambridge University Press, 2014), and we will just assume the reader is familiar
with the necessary material. Indeed, the present book was designed as a companion
volume to the earlier book. A brief summary of what we do assume, and an even
briefer reprise of some essential ideas, is given in the first chapter, but you will
probably not be able to learn enough about Haskell there to understand the rest
of the book. Even if you do know something about functional programming, but
not about how equational reasoning enters the picture (some books on functional
programming simply don’t mention equational reasoning), you will probably still
have to refer to our earlier book. In any case, the mathematics involved in equational
reasoning is neither new nor difficult.
Books on algorithm design traditionally cover three broad areas: a collection of
design principles, a study of useful data structures, and a number of interesting and
intriguing algorithms that have been discovered over the centuries. Sometimes the
books are arranged by principles, sometimes by topic (such as graph algorithms, or
text algorithms), and sometimes by a mixture of both. This book mostly takes the
first approach. It is devoted to five main design strategies underlying many effective
algorithms: divide and conquer, greedy algorithms, thinning algorithms, dynamic
programming, and exhaustive search. These are the design strategies that every
serious programmer should know. The middle strategy, on thinning algorithms,
is new, and serves in many problems as an alternative to dynamic programming.
Preface xv

Each design strategy is allocated a part to itself, and the chapters on each strategy
cover a variety of algorithms from the well-known to the new. There is only a
little material on data structures – only as much as we need. In the first part of the
book we do discuss some basic data structures, but we will also rely on some of
Haskell’s libraries of other useful ways of structuring data. One reason for doing so
is that we wanted the book not to be too voluminous; another reason is that there
does exist one text, Chris Okasaki’s Purely Functional Data Structures (Cambridge
University Press, 1998), that covers a lot of the material. Other books on functional
data structures have been published since we began writing this book, and more are
beginning to appear.
Another feature of this book is that, as well as some firm favourites, it describes a
number of algorithms that do not usually appear in books on algorithm design. Some
of these algorithms have been adapted, elaborated, and simplified from yet another
book published by Cambridge University Press: Pearls of Functional Algorithm
Design (2010). The reason for this novelty is simply to make the book entertaining
as well as instructive. Books on algorithm design are read, broadly speaking, by
three kinds of people: academics who need reference material, undergraduate or
graduate students on a course, and professional programmers simply for interest
and enjoyment. Most professional programmers do not design algorithms but just
take them from a library. Yet they too are a target audience for this book, because
sometimes professional programmers want to know more about what goes into a
good algorithm and how to think about them.
Algorithms in real life are a good deal more intricate than the ones presented in
this book. The shortest-path algorithm in a satellite navigation system is a good
deal more complicated than a shortest-path algorithm as presented in a textbook
on algorithm design. Real-life algorithms have to cope with the problems of scale,
with the effective use of a computer’s hardware, with user interfaces, and with many
other things that go into a well-designed and useful product. None of these aspects
is covered in the present book, nor indeed in most books devoted solely to the
principles of algorithm design.
There is another feature of this book that deserves mention: all exercises are
answered, if sometimes somewhat briefly. The exercises form an integral part of
the text, and the questions and answers should be read even if the exercises are not
attempted. Rather than have a complete bibliography at the end of the book, each
chapter ends with references to (some of) the books and articles pertinent to the
chapter.
Most of the major programs in this book are available on the web site

www.cs.ox.ac.uk/publications/books/adwh
xvi Preface

You can also use this site to see a list of all known errors, as well as report new ones.
We also welcome suggestions for improvement, including ideas for new exercises.

Acknowledgements
Preparation of this book has benefited enormously from careful reading by Sue
Gibbons, Hsiang-Shang Ko, and Nicolas Wu. The manuscript was prepared using
the lhs2TEX system of Ralf Hinze and Andres Löh, which pretty-prints the Haskell
code and also allows it to be extracted and type-checked. The extracted code was
then tested using the wonderful QuickCheck tool developed by Koen Claessen and
John Hughes. Type-checking and QuickChecking the code has saved us from many
infelicities; any errors that remain are, of course, our own responsibility.
We also thank David Tranah and the team at Cambridge University Press for their
advice and hard work in the generation of the final version of the text.

Richard Bird
Jeremy Gibbons
PART ONE

BASICS
3

What makes a good algorithm? There are as many answers to this question as there
are to the question of what makes a good cookbook recipe. Is the recipe clear and
easy to follow? Does the recipe use standard and well-understood techniques? Does
it use widely available ingredients? Is the preparation time reasonably short? Does it
involve many pots and pans and a lot of kitchen space? And so on and so on. Some
people when asked this question say that what is most important about a recipe
is whether the dish is attractive or not, a point we will try to bear in mind when
expressing our functional algorithms.
In the first three chapters we review the ingredients we need for designing good
recipes for attractive algorithms in a functional kitchen, and describe the tools we
need for analysing their efficiency. Our functional language of choice is Haskell,
and the ingredients are Haskell functions. These ingredients and the techniques for
combining them are reviewed in the first chapter. Be aware that the chapter is not
an introduction to Haskell; its main purpose is to outline what should be familiar
territory to the reader, or at least territory that the reader should feel comfortable
travelling in.
The second chapter concerns efficiency, specifically the running time of algo-
rithms. We will ignore completely the question of space efficiency, for the plain
fact of the matter is that executing a functional program can take up quite a lot of
kitchen space. There are methods for controlling the space used in evaluating a
functional expression, but we refer the reader to other books for their elaboration.
That chapter reviews asymptotic notation for stating running times, and explores
how recurrence relations, which are essentially recursive functions for determining
the running times of recursive functions, can be solved to give asymptotic estimates.
The chapter also introduces, albeit fairly briefly, the notion of amortised running
times because it will be needed later in the book.
The final chapter in this part introduces a small number of basic data structures
that will be needed at one or two places in the rest of the book. These are symmetric
lists, random-access lists, and purely functional arrays. Mostly we postpone discus-
sion of any data structure required to make an algorithm efficient until the algorithm
itself is introduced, but these three form a coherent group that can be discussed
without having specific applications in mind.
Chapter 1

Functional programming

Haskell is a large and powerful language, brimming with clever ideas about how
to structure programs and possessing many bells and whistles. But in this book we
will use only a small subset of the host of available features. So, no Monads, no
Applicatives, no Foldables, and no Traversables. In this chapter we will spell out
what we do need to construct effective algorithms. Some of the material will be
revisited when particular problems are put under the microscope, so you should
regard the chapter primarily as a way to check your understanding of the basic ideas
of Haskell.

1.1 Basic types and functions


We will use only simple types, such as Booleans, characters, strings, numbers of
various kinds, and lists. Most of the functions we use can be found in Haskell’s
Standard Prelude (the Prelude library), or in the library Data.List. Be warned that
the definitions we give of some of these functions may not be exactly the definitions
given in these libraries: the library definitions are tuned for optimal performance
and ours for clarity. We will use type synonyms to improve readability, and data
declarations of new types, especially trees of various kinds. When necessary we
make use of simple type classes such as Eq, Ord, and Num, but we will not introduce
new ones. Haskell provides many kinds of number, including two kinds of integer,
Int and Integer, and two kinds of floating-point number, Float and Double. Elements
of Int are restricted in range, usually [−263 , 263 ) on 64-bit computers, though Haskell
compilers are only required to cover the range [−229 , 229 ). Elements of Integer are
unrestricted. We will rarely use the floating-point numbers provided by Float and
Double. In one or two places we will use Rational arithmetic, where a Rational
number is the ratio of two Integer values. Haskell does not have a type of natural
6 Functional programming

numbers,1 though the library Numeric.Natural does provide arbitrary-precision


ones. Instead, we will sometimes use the type synonym
type Nat = Int
Haskell cannot enforce the constraint that elements of Nat be natural numbers, and
we use the synonym purely to document intention. For example, we can assert
that length :: [a] → Nat because the length of a list, as defined in the Prelude,
is a nonnegative element of Int. Haskell also provides unsigned numbers in the
Data.Word library. Elements of Word are unsigned numbers and can represent
natural numbers n in the range 0  n < 264 on 64-bit machines. However, defining
type Nat = Word would be inconvenient simply because we could not then assert
that length :: [a] → Nat.
Most important for our purposes are the basic functions that manipulate lists.
Of these the most useful are map, filter, and folds of various kinds. Here is the
definition of map:
map :: (a → b) → [a] → [b]
map f [ ] = []
map f (x : xs) = f x : map f xs
The function map applies its first argument, a function, to every element of its
second argument, a list. The function filter is defined as follows:
filter :: (a → Bool) → [a] → [a]
filter p [ ] = []
filter p (x : xs) = if p x then x : filter p xs else filter p xs
The function filter filters a list, retaining only those elements that satisfy the given
test. There are various fold functions on lists, most of which will be explained in
due course. Two of the important ones are foldr and foldl. The former is defined as
follows:
foldr :: (a → b → b) → b → [a] → b
foldr f e [ ] =e
foldr f e (x : xs) = f x (foldr f e xs)
The function foldr folds a list from right to left, starting with a value e and using a
binary operator ⊕ to reduce the list to a single value. For example,
foldr (⊕) e [x, y, z] = x ⊕ (y ⊕ (z ⊕ e))
In particular, foldr (:) [ ] xs = xs for all lists xs, including infinite lists. However, we
will not make much use of infinite lists in what follows, except for idioms such as
1 In the documentation for the GHC libraries, there is the statement “It would be very natural to add a type
Natural providing an unbounded size unsigned integer, just as Prelude.Integer provides unbounded size signed
integers. We do not do that yet since there is no demand for it.” Maybe this book will create such a demand.
1.2 Processing lists 7

label :: [a] → [(Nat, a)]


label xs = zip [0 . .] xs
As another example, we can write
length :: [a] → Nat
length = foldr succ 0 where succ x n = n + 1
The second main function, foldl, folds a list from left to right:
foldl :: (b → a → b) → b → [a] → b
foldl f e [ ] =e
foldl f e (x : xs) = foldl f (f e x) xs
Thus
foldl (⊕) e [x, y, z] = ((e ⊕ x) ⊕ y) ⊕ z
For example, we could also write
length :: [a] → Nat
length = foldl succ 0 where succ n x = n + 1
Note that foldl returns a well-defined value only on finite lists; evaluation of foldl
on an infinite list will never terminate. There is an alternative definition of foldl,
namely
foldl f e = foldr (flip f ) e · reverse
where flip is a useful prelude function defined by
flip :: (a → b → c) → b → a → c
flip f x y = f y x
Since one can reverse a list in linear time, this definition is asymptotically as fast as
the former. However, it involves two traversals of the input, one to reverse it and the
second to fold it.

1.2 Processing lists


The difference between foldr and foldl prompts a general observation. When a
programmer brought up in the imperative programming tradition meets functional
programming for the first time, they are likely to feel that many computations seem
to be carried out in the wrong order. Recursion has been described as the curious
process of reaching one’s goal by walking backwards towards it. Specifically, lists
often seem to be processed from right to left when the natural way surely appears to
be from left to right. Appeals to naturalness are often suspicious, and appearances
can be deceptive. We normally read an English sentence from left to right, but when
we encounter a phrase such as “a lovely little old French silver butter knife” the
adjectives have to be applied from right to left. If the knife was made of French
8 Functional programming

silver, but not necessarily made in France, we have to write “a lovely little old
French-silver butter knife” to avoid ambiguity. Mathematical expressions too are
usually understood from right to left, certainly those involving a chain of functional
compositions. As to deceptiveness, the definition
head = foldr () ⊥ where x  y = x
though a little strange is certainly correct and takes constant time. The evaluation
of foldr (), conceptually from right to left, is abandoned after the first element is
encountered. Thus
head (x : xs) = foldr () ⊥ (x : xs)
= x  foldr () ⊥ xs
=x
The last step follows from the fact that Haskell is a lazy language in which eval-
uations are performed only when needed, so evaluation of  does not require
evaluation of its second argument.
Sometimes the direction of travel is important. For example, consider the follow-
ing two definitions of concat:
concat1 , concat2 :: [[a]] → [a]
concat1 = foldr (++) [ ]
concat2 = foldl (++) [ ]
We have concat1 xss = concat2 xss for all finite lists xss (see Exercise 1.10), but
which definition is better? We will look at the precise running times of the two
functions in the following chapter, but here is one way to view the problem. Imagine
a long table on which there are a number of piles of documents. You have to assemble
these documents into one big pile ensuring that the correct order is maintained, so
the second pile (numbering from left to right) has to go under the first pile, the third
pile under the second pile, and so on. You could start from left to right, picking up
the first pile, putting it on top of the second pile, picking the combined pile up and
putting it on top of the third pile, and so on. Or you could start at the other end,
placing the penultimate pile on the last pile, the antepenultimate pile on top of that,
and so on (even English words are direction-biased: the words ‘first’, ‘second’, and
‘third’ are simple, but ‘penultimate’ and ‘antepenultimate’ are not). The left to right
solution involves some heavy lifting, particularly at the last step when a big pile
of documents has to be lifted up and placed on the last pile, but the right to left
solution involves picking up only one pile at each step. So concat1 is potentially a
much more efficient way to concatenate a list of lists than concat2 .
Here is another example. Consider the problem of breaking a list of words into
a list of lines, ensuring that the width of each line is at most some given bound.
This problem is known as the paragraph problem, and there is a section devoted
1.3 Inductive and recursive definitions 9

to it in Chapter 12. It seems natural to process the input from left to right, adding
successive words to the end of the current line until no more words will fit, in
which case a new line is started. This particular algorithm is a greedy one. There
are also non-greedy algorithms for the paragraph problem that process words from
right to left. Part Three of the book is devoted to the study of greedy algorithms.
Nevertheless, these two examples apart, the direction of travel is often unimportant.
The direction of travel is also related to another concept in algorithm design,
the notion of an online algorithm. An online algorithm is one that processes a list
without having the entire list available from the start. Instead, the list is regarded
as a potentially infinite stream of values. Consequently, any online algorithm for
solving a problem for a given stream also has to solve the problem for every prefix
of the stream. And that means the stream has to be processed from left to right. In
contrast, an offline algorithm is one that is given the complete list to start with, and
can process the list in any order it wants. Online algorithms can usually be defined
in terms of another basic Haskell function scanl, whose definition is as follows:
scanl :: (b → a → b) → b → [a] → [b]
scanl f e [ ] = [e]
scanl f e (x : xs) = e : scanl f (f e x) xs
For example,
scanl (⊕) e [x, y, z, ...] = [e, e ⊕ x, (e ⊕ x) ⊕ y, ((e ⊕ x) ⊕ y) ⊕ z, ...]
In particular, scanl can be applied to an infinite list, producing an infinite list as
result.

1.3 Inductive and recursive definitions


While most functions make use of recursion, the nature of the recursion is different
in different functions. The functions map, filter, and foldr all make use of structural
recursion. That is, the recursion follows the structure of lists built from the empty
list [ ] and the cons constructor (:). There is one clause for the empty list and another,
recursive clause for x : xs in terms of the value of the function for xs. We will call
such definitions inductive definitions. Most inductive definitions can be expressed
as instances of foldr. For example, both map and filter can be so expressed (see the
exercises).
Here is another example, an inductive definition of the function perms that returns
a list of all the permutations of a list (we call it perms1 because later on we will
meet another definition, perms2 ):
perms1 [ ] = [[ ]]
perms1 (x : xs) = [zs | ys ← perms1 xs, zs ← inserts x ys]
10 Functional programming

The permutations of a nonempty list are obtained by taking each permutation of


the tail of the list and returning all the ways the first element can be inserted. The
function inserts is defined by
inserts :: a → [a] → [[a]]
inserts x [ ] = [[x]]
inserts x (y : ys) = (x : y : ys) : map (y:) (inserts x ys)
For example,
inserts 1 [2, 3] = [[1, 2, 3], [2, 1, 3], [2, 3, 1]]
The definition of perms1 uses explicit recursion and a list comprehension, but
another way is to use a foldr:
perms1 = foldr step [[ ]] where step x xss = concatMap (inserts x) xss
The useful function concatMap is defined by
concatMap :: (a → [b]) → [a] → [b]
concatMap f = concat · map f
Observe that since
step x xss = (concatMap · inserts) x xss
the definition of perms1 can be expressed even more briefly as
perms1 = foldr (concatMap · inserts) [[ ]]
The idiom foldr (concatMap · steps) e will be used frequently in later chapters for
various definitions of steps and e, so keep the abbreviation in mind.
Here is another way of generating permutations, one that is recursive rather than
inductive:
perms2 [ ] = [[ ]]
perms2 xs = [x : zs | (x, ys) ← picks xs, zs ← perms2 ys]
picks :: [a] → [(a, [a])]
picks [ ] = []
picks (x : xs) = (x, xs) : [(y, x : ys) | (y, ys) ← picks xs]
The function picks picks an arbitrary element from a list in all possible ways,
returning both the element and what remains. The function perms2 computes a
permutation by picking an arbitrary element of a nonempty list as a first element,
and following it with a permutation of the rest of the list.
The function perms2 uses a list comprehension, but an equivalent way is to write
perms2 [ ] = [[ ]]
perms2 xs = concatMap subperms (picks xs)
where subperms (x, ys) = map (x:) (perms2 ys)
1.4 Fusion 11

Expressing perms2 in this way rather than by a list comprehension helps with
equational reasoning, and also with the analysis of its running time. We will return
to both perms1 and perms2 in the following chapter.
The different styles, recursive or inductive, of the definitions of basic combinato-
rial functions, such as permutations, partitions, or subsequences, lead to different
kinds of final algorithm. For example, divide-and-conquer algorithms are usually
recursive, while greedy and thinning algorithms are usually inductive. To appreciate
that there may be different algorithms for one and the same problem, one has to go
back to the definitions of the basic functions used in the specification of the problem
and see if they can be defined differently. For example, the inductive definition of
perms1 leads to Insertion sort, while the recursive definition of perms2 leads to Se-
lection sort. These two sorting algorithms will be introduced in the context of greedy
algorithms in Part Three. The general point is a key one for functional algorithm
design: different solutions for problems arise simply because there are different
but equally clear definitions of one or more of the basic functions describing the
solution.
While functional programming relies solely on recursion to define arbitrarily long
computations, imperative programming can also make use of loops of various kinds,
including while and until loops. We can define and use loops in Haskell too. For
example,
until :: (a → Bool) → (a → a) → a → a
until p f x = if p x then x else until p f (f x)
is a recursive definition of the function until that repeatedly applies a function to a
value until the result satisfies some condition. We will encounter until again later in
the book. Given until we can define while by
while p = until (not · p)
We can also define a functional version of simple for-loops in which a function is
applied to an argument a specified number of times (see the exercises).

1.4 Fusion
The most powerful technique for constructing efficient algorithms lies in our ability
to fuse two computations together into one computation. Here are three simple
examples:
map f · map g = map (f · g)
concatMap f · map g = concatMap (f · g)
foldr f e · map g = foldr (f · g) e
12 Functional programming

The first equation says that the two-step process of applying one function to every
element of a list, and then applying a second function to every element of the
result, can be replaced by a one-step traversal in which the composition of the two
functions is applied to each element. The second equation is an instance of the first
one, and the third is yet another example of when two traversals can be replaced by
a single traversal.
Here is an another example of a fusion law, one for you to solve:
foldr f e · concat = ????
Pause for a minute or so to try and complete the right-hand side. It is a good test of
your understanding of the material so far. But do not be discouraged if you cannot
find the answer, because it is not too obvious and many experienced functional
programmers would fail to spot it. In a moment we will show how this particular
fusion rule follows from one single master rule. Indeed, that is how we ourselves
know the right-hand side, not by memorising it but by reconstructing it from the
master rule.

You probably paused for a short time, gave up and then read on. But you don’t
get away that easily. Try this simpler version first:
foldr f e (xs ++ ys) = ????
Having answered this question, can you now answer the first one?

The answers to both questions will be given shortly. The master fusion rule is the
fusion law of foldr. This law states that
h (foldr f e xs) = foldr g (h e) xs
for all finite lists xs provided
h (f x y) = g x (h y)
for all x and y. The proviso is called the fusion condition. Without one extra proviso,
the restriction to finite lists is necessary (see the exercises). The proof of the fusion
rule is by induction on the structure of a list. There are two cases, a base case and
an induction step. The base case is
h (foldr f e [ ])
= { definition of foldr }
he
= { definition of foldr }
foldr g (h e) [ ]
The induction step is
Another Random Scribd Document
with Unrelated Content
be freely shared with anyone. For forty years, he produced and
distributed Project Gutenberg™ eBooks with only a loose
network of volunteer support.

Project Gutenberg™ eBooks are often created from several


printed editions, all of which are confirmed as not protected by
copyright in the U.S. unless a copyright notice is included. Thus,
we do not necessarily keep eBooks in compliance with any
particular paper edition.

Most people start at our website which has the main PG search
facility: www.gutenberg.org.

This website includes information about Project Gutenberg™,


including how to make donations to the Project Gutenberg
Literary Archive Foundation, how to help produce our new
eBooks, and how to subscribe to our email newsletter to hear
about new eBooks.
Welcome to our website – the ideal destination for book lovers and
knowledge seekers. With a mission to inspire endlessly, we offer a
vast collection of books, ranging from classic literary works to
specialized publications, self-development books, and children's
literature. Each book is a new journey of discovery, expanding
knowledge and enriching the soul of the reade

Our website is not just a platform for buying books, but a bridge
connecting readers to the timeless values of culture and wisdom. With
an elegant, user-friendly interface and an intelligent search system,
we are committed to providing a quick and convenient shopping
experience. Additionally, our special promotions and home delivery
services ensure that you save time and fully enjoy the joy of reading.

Let us accompany you on the journey of exploring knowledge and


personal growth!

textbookfull.com

You might also like