You are on page 1of 287
cornerstones of computing series editors: richard bird & tony hoare the fun of programming jeremy gibbons and oege de moor . PRE-J The Fun of Programming Edited by Jeremy Gibbons and Oege de Moor Preface and Editorial Matter © Jeremy Gibbons and Oege de Moor 2003 Individual chapters © contributors 2003 All rights reserved. No reproduction, copy or transmission of, this publication may be made without written permission. No paragraph of this publication may be reproduced, copied or transmitted save with written permission or in accordance with the provisions of the Copyright, Designs and Patents Act 1988, or under the terms of any licence permitting limited copying issued by the Copyright Licensing Agency, 90 Tottenham Court Road, London W1T 4LP. ‘Any person who does any unauthorised act in relation to this publication may be liable to criminal prosecution and civil claims for damages. The authors have asserted their rights to be identified as the authors of this work in accordance with the Copyright, Designs and Patents Act 1988. First published 2003 by PALGRAVE MACMILLAN Houndmills, Basingstoke, Hampshire RG21 6XS and 175 Fifth Avenue, New York, N. Y. 10010 Companies and representatives throughout the world PALGRAVE MACMILLAN is the global academic imprint of the Palgrave Macmillan division of St. Martin's Press, LLC and of Palgrave Macmillan Ltd. Macmillan® is a registered trademark in the United States, United Kingdom and other countries, Palgrave is a registered trademark in the European Union and other countries. ISBN 978- 1-4039-0772-1 hardback ISBN 978-0-333-99285-2 paperback This book is printed on paper suitable for recycling and made from fully ‘managed and sustained forest sources. Logging, pulping and manufacturing processes are expected to conform to the environmental regulations of the country of origin, A catalogue record for this book is available from the British Library. wo 987654321 12 11 10 09 08 07 06 05 04 03 Printed and bound in Great Britain by CPI Antony Rowe, Chippenham and Eastbourne Preface 1 Contents Fun with binary heap trees Chris Okasaki apUanauN Binary heap trees Maxiphobic heaps Persistence Round-robin heaps Analysis of skew heaps Lazy evaluation Analysis of lazy skew heaps Chapter notes Specification-based testing with QuickCheck Koen Claessen and John Hughes 2.1 2.2 2.3 2.4 2.5 2.6 27 2.8 Introduction Properties in QuickCheck Example: Developing an abstract data type of queues Quantifying over subsets of types Test coverage A larger case study Conclusions Acknowledgements Origami programming Jeremy Gibbons 3.1 3.2 3.3 3.4 35 3.6 Introduction Origami with lists: sorting Origami by numbers: loops Origami with trees: traversals Other sorts of origami Chapter notes 17 17 18 20 25 30 32 39 39 4l 41 42 49 52 56 60 Describing and interpreting music in Haskell Paul Hudak 4.1 Introduction 4.2 Representing music 4.3 Operations on musical structures 4.4 The meaning of music 4.5 Discussion Mechanising fu: Ganesh Sittampalam and Oege de Moor 5.1 Active source 5.2 Fusion, rewriting and matching 5.3 The MAG system 5.4 Asubstantial example 5.5. Difficulties 5.6 Chapter notes How to write a financial contract Simon Peyton Jones and Jean-Marc Eber 6.1 Introduction 6.2 Getting started 6.3. Building contracts 6.4 Valuation 6.5 Implementation 6.6 Operational semantics 6.7 Chapter notes Functional images Conal Elliott 7.1 Introduction 7.2 What is an image? 7.3 Colours 7.4 Pointwise lifting 7.5 Spatial transforms 7.6 Animation 7.7 Region algebra 7.8 Some polar transforms 7.9 Strange hybrids 7.10 Bitmaps 7.11 Chapter notes Functional hardware description in Lava Koen Claessen, Mary Sheeran and Satnam Singh 8.1 Introduction 8.2 Circuits in Lava 8.3 Recursion over lists 61 61 61 67 70 78 79 79 85 89 98 101 103 105 105 106 108 16 123 127 128 131 131 132 135 137 139 141 142 144 147 148 150 1st 1st 152 153 10 12 8.4 8.5 8.6 8.7 88 8.9 8.10 Connection patterns Properties of circuits Sequential circuits Describing butterfly circuits Batcher's mergers and sorters Generating FPGA configurations Chapter notes Combinators for logic programming Michael Spivey and Silvija Seres 9.1 9.2 9.3 9.4 9.5 9.6 9.7 9.8 9.9 Introduction Lists of successes Monads for searching Filtering with conditions Breadth-first search Lifting programs to the monad level Terms, substitutions and predicates Combinators for logic programs Recursive programs Arrows and computation Ross Paterson 10.1 10.2 10.3 10.4 10.5 Notions of computation Special cases Arrow notation Examples Chapter notes A prettier printer Philip Wadler Ww 11.2 11.3 14 1s 11.6 W7 Introduction A simple pretty printer A pretty printer with alternative layouts Improving efficiency Examples Chapter notes Code Fun with phantom types Ralf Hinze 12.1 12.2 12.3 12.4 12.5 12.6 Introducing phantom types Generic functions Dynamic values Generic traversals and queries Normalisation by evaluation Functional unparsing 155 157 160 162 166 170 175 177 177 178 179 182 184 187 188 191 193 201 201 208 213 216 222 223 223 224 228 233 236 238 240 245 245 248 250 252 255 257 vi 12.7 Atype equality type 12.8 Chapter notes Bibliography Index 259 262 263 273 Preface Functional programming has come of age: it is now a standard course in any computer science curriculum. Ideas that were first developed in the laboratory environment of functional programming have proved their value in wider settings, such as generic Java and XML. The time is ripe, therefore, to teach a second course on functional programming, delving deeper into the subject. This book is the text for such a course. ‘The emphasis is on the fun of programming in a modern, well designed programming language such as Haskell. There are chapters that focus on applications, in particular pretty printing, musical composition, hardware de- scription, and graphical design. These applications are interspersed with chap- ters on techniques, such as the design of efficient data structures, interpreters for other languages, program testing and optimisation. These topics are of interest to every aspiring programmer, not just to those who choose to work in a functional language. Haskell just happens to be a very convenient vehicle for expressing the ideas, and the theme of functional programming as a lingua franca to communicate ideas runs throughout the book. The prerequisites for this material are covered in any introductory course on functional programming. In fact, it is a seamless sequel to courses that are based on An introduction to functional programming using Haskell by Richard Bird [15]. Throughout the text, references to that book are made by the ab- breviation ‘IFPH’. The present volume could also be used as a sequel to other introductory books, however. All that is expected of the reader is a working knowledge of higher-order functions and polymorphism, and an understand- ing of lazy evaluation. Many of the chapters in this book are accompanied by software, which can be found on the website http://web.comlab.ox.ac.uk/oucl/publications/books/fop As the book is adopted for courses by others, we shall be happy to add links to further teaching materials. This book was produced to celebrate the work of Richard S. Bird on his sixtieth birthday. For many years, Richard has led the development of func- tional programming, in particular in the area of synthesising programs from viii specifications. Apart from these research contributions, he educated many generations of programmers through his textbooks. When the question of a festschrift came up, it was immediately evident that it should be a textbook of lasting value, written by his friends in the research community. Above all, we hope it conveys Richard's sense of fun in the subject, which has delighted us all. For this reason we have borrowed the title of one of Richard's own lectures: The Fun of Programming. Many happy returns, Richard! We would like to thank our editors at Palgrave, Tracey Alcock, Esther Thackeray and Rebecca Mashayekh, for their efficient help in the production of this book. Andres Léh gave sterling help with thorough last-minute reviewing. Jeremy Gibbons Oege de Moor Oxford, September 2002 Fun with binary heap trees Chris Okasaki The world of data structures takes on a new fascination when viewed through the lens of functional programming. Two issues in particular set functional data structures apart from typical imperative data structures: persistence and laziness. In this chapter, we will explore these and other issues in the context of priority queues implemented as binary heap trees. 1.1 Binary heap trees A flock of chickens naturally forms a pecking order, a strict dominance hierarchy in which hens literally peck at other hens lower in the hierarchy and submit meekly to pecking by those higher in the hierarchy. Binary heap trees (IFPH §6.3) are close cousins of binary search trees. Like binary search trees, binary heap trees are binary trees with ordered labels. They can be represented by the datatype data (Ord x) =» Tree o = Null | Fork o (Tree ox) (Tree x) The major difference between search trees and heap trees is the ordering invariant on labels. In a heap tree, the sequence of labels along any path from the root to a leaf must be non-decreasing. In other words, the label of every child node must be at least as big as its parent's. One immediate consequence of this invariant is that the smallest label in a tree is always at the root. Figure 1.1 shows several examples of binary heap trees. Notice that heap trees place no restrictions on sibling nodes. We know that the labels of both are greater than or equal to the label of the parent, but we have no idea which of the siblings is smaller. The same is true for cousin nodes, or any other pair of nodes where neither is a descendant of the other. Notice also that heap trees are not required to be balanced. In fact, the more unbalanced a tree is, the faster many heap-tree algorithms run! (See Exercise 1.3.) 2 The Fun of Programming 1 \ 7 \ 7 2 5 \ 7 7 \ 4 2 \ / \ 4 4 6 Figure 1.1: Several examples of binary heap trees. Priority queues Binary heap trees are often used to represent priority queues supporting at least the following set of operations:! isEmpty :: Tree a ~ Bool — is the heap empty? minElem :: Tree a ~ « — find the smallest element deleteMin: Tree x ~ Tree x — delete the smallest element insert. a ~ Treea ~ Treex — add a single element merge :: Treea ~ Treea ~ Tree — combine two heaps As is traditional, although admittedly somewhat confusing, smaller labels mean higher priorities, so the minimum element is the element with the high- est priority. The isEmpty and minElem operations are trivial to implement. isEmpty Null = True isEmpty (Fork xab) = False minElem (Fork x ab) = x Both clearly run in O(1) time. The deleteMin and insert operations are also trivial to implement, given an implementation of merge: deleteMin (Fork x ab) merge ab insert xa merge (Fork x Null Null) a Assuming merge runs in O(logn) time, so do deleteMin and insert. All that remains is to come up with an implementation of merge that runs in O(log n) time. 'Some implementations of priority queues omit merge, but we will not discuss such imple- mentations further. 1 Fun with binary heap trees 3 Merge There are actually many different ways to implement merge for binary heap trees, each leading to a different kind of heap. Examples include leftist heaps [29, 80], weight-biased leftist heaps [22] (see also IFPH §8.4), and skew heaps {120}. Portions of the merge operation are straightforward. Merging any heap with an empty heap yields the original heap. Combining two non-empty heaps with root labels x and y, respectively, produces a tree whose root label is the smaller of x and y. These facts are enough to write part of the merge function. merge a Null a merge Null b b merge ab | minElema < minElem b= joinab | otherwise = joinba Join (Fork x ab) c = Forkx?? The auxiliary function join is called once we've decided which root label is smaller, That label becomes the root label of the result, but what should the subtrees of this new node be? We have three trees in hand—a, b, and c—but only two slots! The easiest way to reduce three trees to two is to merge two of them. Unfortunately, there are at least six ways to do this, depending on which trees are merged and how the resulting trees are arranged: Fork xa (merge bc) Fork xb (merge ac) Fork x (merge ab) Fork x (merge bc) a Fork x (merge ac) b Fork x (merge ab) c None of these has an obvious advantage over the others, so which should we choose? If we think back to binary search trees, most techniques for balancing require some extra information in each node, such as a count or a colour. Of course, we are not trying to balance the heap trees, but perhaps extra information in each node would allow us to decide intelligently between the six possibilities above. Exercise 1.1 Two of the six possible strategies for join would, if followed exclusively, lead to all trees being linear in shape. Which two? 0 4 The Fun of Programming 1.2 Maxiphobic heaps Most ravens can count to about four. If two hunters enter a hunting blind and one comes out a short time later, most birds will be fooled into thinking that the blind is now empty. But not ravens! To fool a raven, you need to send roughly six hunters into the blind and have five come out. Our goal is to implement merge so that it runs in O(logn) time. What we have so far for merge involves a little pattern matching, a comparison, a few trivial function calls, a recursive call to merge, and a constructor. Assuming comparisons take O(1) time, the whole function takes time proportional to the depth of recursion. Therefore, we want a scheme that will keep the recursion as shallow as possible. Intuitively, we would expect merge to take longer for bigger trees than for smaller trees. Therefore, given a choice, we should choose to merge smaller trees rather than bigger trees. We have no control over external calls to merge (that is, calls by the user), nor do we have any flexibility in the internal calls to merge by deleteMin and insert. However, we do have a choice in the join function. We need to merge two of the three trees a, b, and c, but we can choose which trees to merge however we wish. Following the above heuristic, we choose to merge the two smallest trees, leaving the biggest tree untouched. We dub such heaps maxiphobic (‘biggest avoiding’). To implement maxiphobic heaps, we need to extend our tree datatype with a count field that keeps track of the size of each tree. data (Orda) > Tree c = Null | Fork Int oc (Tree cx) (Tree cx) The original operations are nearly unaffected by this change. We add the extra count field to the Fork patterns in the isEmpty, minElem, and deleteMin operations, and an initial size of 1 to the newly created singleton node in the insert operation. The merge operation is unchanged. isEmpty Null True isEmpty (Fork nx ab) False minElem (Fork nx ab) =x deleteMin (Fork nx ab) = mergeab insert xa = merge (Fork 1 x Null Null) a merge a Null a merge Null b =b merge ab | minElema < minElem b = joinab | otherwise yoinba The main change is in the join function, which must now decide which of the 1 Fun with binary heap trees 5 lo 1 1 7 7 \ 7 \ 2 2 3 2 3 / 1 1 4 1 37> 37> ™, / ‘o/s 7\. SN 7\ 4 5 4 5 65 6 4 7 Figure 1.2: The results of inserting the numbers 1-7 into an initially empty maxiphobic heap. three trees is biggest. Jjoin(Forknxab)c = Fork (n+ sizec) x aa (merge bb cc) where (aa, bb, cc) -orderBySize abc orderBySizeabc | sizea = biggest = (a,b,c) | size b = biggest = (b,a,c) | sizec = biggest = (c,a,b) where biggest = sizea ‘max’ sizeb ‘max’ sizec size Null =0 size (Fork nxab) =n Figure 1.2 illustrates a sequence of insertions using these rules for join. Does this implementation run in O(log) time? Yes. Suppose that the combined size of the original arguments to merge was n. Then the combined size of a, b, and c in join is n- 1. The largest of these has size at least [54]. Therefore, the combined size of the two trees passed to the recursive merge is at most |2"=2}, or roughly two-thirds n. Therefore, there can be at most O(log1.s 2) = O(log n) recursive calls. Exercise 1.2 Draw the trees resulting from the following operations: 1. Insert the numbers 1-7 into an initially empty heap in reverse order. 2. Insert the numbers 1, 7, 2, 6, 3, 5, 4 into an initially empty heap. 3. Delete the minimum element from the last tree of Figure 1.2. o Exercise 1.3 Explain why an unbalanced maxiphobic heap (in the extreme, a completely linear heap) is preferable to a balanced one. 0 6 The Fun of Programming original heap new heap 1 Sy \ fuer 8 4 Figure 1.3: Insertion into a persistent maxiphobic heap. After inserting the number 7 into the original heap, the original heap still exists. The dashed lines indicate links to shared nodes. 1.3. Persistence The Sankofa bird is a Ghanaian symbol depicted as a bird looking backwards. It symbolises the idea that one must return to the past to build for the future. So far, the description of maxiphobic heaps has been essentially language neu- tral. The code fragments have been given in Haskell, but they could just as easily have been given in, say, C or Smalltalk. However, the natural expression of this data structure in Haskell contains a subtlety that makes it characteris- tically functional. This subtlety occurs in a single line of the join function: Join (Fork nx ab) c = Fork (n + size c) x aa (merge bb cc) Here is the question that distinguishes functional data structures from im- perative ones: Does the right-hand side of this line denote the creation of a new Fork node or the in-place modification of the old one? This innocent- sounding question has profound implications on the design, implementation, and analysis of data structures. If join destructively modifies the existing node, as it would in most imper- ative implementations, then the data structure is ephemeral. An ephemeral data structure exists in the perpetual present—it has no appreciation for or access to the resources of the past. For example, suppose we have a heap containing the elements 1, 2, and 3. If we insert the number 4, then the heap will contain the elements 1, 2, 3, and 4. The old version of the heap—the one containing only 1, 2, and 3—no longer exists. On the other hand, if join creates a new Fork node, as it would in most functional implementations, then the data structure is persistent. A persistent data structure allows equal access to any current or past version of the data structure, Suppose we again have a heap containing the elements 1, 2, and 3. If we insert the number 4, we get a new heap containing the elements 1, 2, 3, and 4, but the old heap containing 1, 2, and 3 still exists. Future heap 1 Fun with binary heap trees 7 operations can use either heap with equal ease. Figure 1.3 illustrates how a functional implementation of maxiphobic heaps achieves persistence. Beginning with a heap containing the numbers 1, 2,3, 4, 5, 6, and 8, we insert the number 7. Three new nodes are created (two by join and one by insert), containing the numbers 1, 4, and 7, respec- tively. The resulting heap comprises those three nodes, plus two subtrees inherited from the original heap—the subtree containing 2, 3, 5, and 6 and the subtree containing 8. Because we have not modified any of the existing nodes, the original heap co-exists in memory with the new heap, with certain subtrees belonging equally to both. This sharing of nodes between versions of a data structure is crucial to the efficiency of this style of persistence. It is safe only if we never modify a shared node. Otherwise, we might unintentionally change multiple instances of a data structure when we only wanted to change one. For example, if we were to try to change the 8 to a 9 by modifying the node in place rather than creating a new node, we would affect both the original heap (without the 7) and the new heap (with the 7). Exercise 1.4 In the sequence of heaps presented in Figure 1.2, which node or nodes are shared among the most heaps? (Hint: It is not the node containing the number 1 because there are actually seven different nodes containing the number 1, one per heap.) 0 1.4 Round-robin heaps When geese fly in a vee formation, the lead goose works the hardest. To avoid overtaxing any single bird, the geese in the flock take turns flying in the point position. Maxiphobic heaps are already simple and efficient, but we can still do better. In this section, we will dramatically simplify the implementation of heaps without changing the running times of the heap operations. This is not the mythical free lunch, however. The price will be a corresponding increase in the difficulty of proving the time bounds. Imagine you are dealer in a game of bridge. You need to distribute 52 cards evenly among four players, 13 cards each. This is a social game, so you want to be able to chat with your friends while dealing. How do you deal? You might attempt to deal 13 cards to the first player, followed by 13 cards to the second player, and so forth. Unfortunately, this approach requires accurate counting, which can be rather challenging in the middle of a conversation. A simpler approach is to deal the cards in a round-robin fashion: one card to each player, followed by a second card to each player, and so forth. This task is cognitively much simpler. Rather than counting, you only need to keep track of who received the most recent card, which the position of your hands will usually tell you. 8 The Fun of Programming data Colour Blue | Red data (Ord x) = Tree « Null | Fork Colour o (Tree ) (Tree &) isEmpty Null = True isEmpty (Fork col x a b) = False ‘minElem (Fork col x a b) deleteMin (Fork col x ab) x = mergeab insert xa ‘merge (Fork Blue x Null Null) a merge a Null a merge Null b b merge ab | minElema < minElem b= joinab | otherwise = joinba Join (Fork Blue x.ab) ¢ Join (Fork Red xab) c Fork Red x (merge ac) b Fork Blue x a (merge bc) Figure 1.4: The implementation of round-robin heaps. Can we apply the same intuition to heaps? Looking back at Figure 1.2, we see that, for increasing sequences at least, the end result of counting and comparing tree sizes is to distribute the incoming nodes evenly among the subtrees at any level of the heap. But surely we can achieve this result without storing an entire integer at each node! All we need is a scheme for each individual node to distribute incoming nodes evenly between its two subtrees. We dub the resulting heaps round-robin heaps. We colour each node to indicate which of its two subtrees should receive the next incoming node. A node coloured robin’s-egg blue sends the next incoming node to its left subtree, and a node coloured robin's-breast red sends the next incoming node to its right subtree. It does not matter which colour we use for new nodes, so we arbitrarily choose blue. The implementation is straightforward, with the join function using the colour to determine whether to merge c with a or b and then changing the colour for next time. Figure 1.4 shows the code for round-robin heaps, and Figure 1.5 illustrates a sequence of insertions into a sample heap. The join function for round-robin heaps is significantly shorter and sim- pler than the join function for maxiphobic heaps, but we are not finished yet. Instead of using a colour to encode which subtree gets the next incoming node, we can use the two subtree slots in the Fork constructor as a tiny, fixed-size FIFO queue. The left subtree is the front of the queue, and the right subtree is the back of the queue. When the front subtree gets the next incoming node, 1 Fun with binary heap trees 9 Ip Ir Ly Ik / aN 7'\ 22 eek / 45 le lk Ip ZN ZN aN 2k 3r 3k 2p 35 / OTN / oN 7s 45 ny | Figure 1.5: The results of inserting the numbers 1-7 into an initially empty round-robin heap. The subscripts indicate the colour of each node. it moves to the back of the queue, and the subtree that used to be in the back moves up to the front. This implementation, known as skew heaps [120], is shown in Figure 1.6.? It is hard to imagine that any implementation could be simpler! The Tree datatype and the heap operations return to their original definitions (from Section 1.1), and join becomes simply Join (Fork xa b) c = Fork x b (merge ac) Figure 1.7 illustrates a sequence of insertions into a sample skew heap. Exercise 1.5 Come up with sequences of insertions and merges that will result in the following round-robin heaps: 9s 8B 8a OR What do the same sequences produce using skew heaps? 0. Most presentations of skew heaps reverse the meaning of the left and right subtrees, treating the right as the front and the left as the back. However, this difference is insignificant. 10 ‘The Fun of Programming data (Ord x) > Tree o = Null | Fork « (Tree 0) (Tree ) isEmpty Null True isEmpty (Fork x ab) False minElem (Fork x ab) =x deleteMin (Fork x a b) = mergeab insert xa = merge (Fork x Null Null) @ ‘merge a Null a merge Null b b merge ab | minElema < minElem b | otherwise join (Fork x ab) c = Fork x b (merge ac) Figure 1.6: The implementation of skew heaps. 1.5 Analysis of skew heaps In the daytime, when the sun is out, the owl goes deep into the hollow and sleeps. That is, they say he sleeps, but I don’t believe it. How could anyone sleep so long? I think he sits in there, part of the time at least, and thinks, And that's why he knows so much. - Robert C. O'Brien, The Secret of NIMH Figure 1.7 suggests an O(log n) bound for insertions into round-robin or skew heaps, but Exercise 1.5 dashes that hope. An insertion or merge involving one of those trees may take O(n) time in the worst case! On the other hand, the sequences of operations leading up to the trees in Exercise 1.5 take only O(n) time, rather than O(nlog n) time, so even if the next insertion takes another O(n) steps, we still come out ahead. If we are willing to accept amortised rather than worst-case bounds, we can show that merge, insert, and deleteMin still run in O(log n) time. An amortised analysis [127] looks at the worst case for a sequence of operations, rather than for a single operation. The cost for the entire sequence is averaged over the all the operations in the sequence. It is expected that certain individual operations will take more than the average, but this does no harm as long as enough other operations take less than the average. We will use the accounting notion of credits [127] to analyse skew heaps. Each credit pays for a constant amount of work, and no work may be performed without a corresponding credit. Each operation is allocated a certain number 1 Fun with binary heap trees 4 1 1 57> ™, 7\ SN 7\ 4 64 6 5 7 \ 5 5 Figure 1.7: The results of inserting the numbers 1-7 into an initially empty skew heap. of credits, and any unused credits at the end of the operation are saved for use by future operations that run out of their own credits. If we can prove that we never run out of credits, then we know that our total running time is bounded by the total number of credits allocated, and we claim that each operation runs in amortised time proportional to the number of credits allocated to that operation. As an analogy, imagine that you work for a company that allows 15 sick days per year, and that unused sick days may be saved for future years. As long as you never run out of sick days, you can be sure that you have spent no more than 15 sick days per year in an amortised sense, even if you happened to spend several years’ worth of sick days in one particularly unhealthy year. Applying these ideas to skew heaps, we will show that allocating O(log n) credits per operation is enough to never run out, and therefore that each operation runs in O(log n) amortised time. Define a node to be good if its right subtree is at least as big as its left, and bad if its left subtree is bigger than its right. We insist that every bad node carry a single spare credit. Consider a merge of two trees T; and T} of sizes m and mp, respectively, and let n = m) + mp. We will show that such a merge requires O(log im + log mz) = O(log n) credits. Suppose that there are a total of k + 1 merge steps, the first k of which invoke the join function. Join (Fork x ab) c = Fork x b (merge.ac) ‘There are two cases for each of the k join steps: © If Fork x a bis bad, then we use its credit to pay for this step. The resulting node is guaranteed to be good. « If Fork x ab is good, then the resulting node may be bad or good, depend- ing on the size of c. We need at most two credits: one to pay for this step and one to give to the resulting node in case it is bad. 12 The Fun of Programming Altogether, we allocate at most 2g + 1 credits for the entire merge, where g is the number of good nodes encountered by join and the extra credit pays for the terminal merge step (which involves a Null), All that remains is to show that g < log m, + log mp. If Forkxab is good, then |a| < |b|. Therefore, in the recursive call merge ac, the number of elements from the tree containing @ has been de- creased by more than half. This can happen to 7; no more than log m, times, and to T> no more than log m times. Therefore, g < logm; + log mp and we conclude that merge runs in O(log n) amortised time, from which we conclude that insert and deleteMin do also. Exercise 1.6 Consider a skew heap built as suggested by Exercise 1.5. How many credits does such a tree have? If you then insert the number 10, how many credits does the new tree have? 0 1.6 Lazy evaluat Sighed Mayzie, a lazy bird hatching an egg: I'm tired and I'm bored And I've kinks in my leg From sitting, just sitting here day after day. It's work! How I hate it! I'd much rather play! - Dr. Seuss, Horton Hatches the Egg Unfortunately, the proof in the previous section fails if we allow persistence. Inserting into a tree with lots of bad nodes uses up some of the credits on those nodes. If we then take advantage of persistence and do another insertion into the original tree, as opposed to the tree resulting from the first insertion, we may run out of credits, because the credits we need from the tree's bad nodes have already been spent. We still have one more trick up our sleeves, however—lazy evaluation. Haskell is a lazy language, meaning that function calls are not executed until their results are needed. As with persistence, the key to understanding lazy evaluation lies in the join function: Join (Fork x ab) c = Fork x b (merge ac) Here is the question that distinguishes lazy languages such as Haskell from strict languages such as Standard ML: Is the recursive merge in the right-hand side of this line executed as part of the join, or does join simply build the new Fork node and leave the merge unevaluated? Again, the answer to this question has profound implications on the design, implementation, and analysis of data structures. If join executes the recursive merge immediately, as it would in a strict language, then data structures such as skew heaps cannot be made persistent 1 Fun with binary heap trees 13 in the functional style without invalidating their amortised time bounds. In- tuitively, the reason is that persistence encourages us to reuse old versions of data structures, but once the credits of a particular version have been spent, they cannot be spent again. On the other hand, if join merely saves the information necessary to exe- cute the recursive merge later, as it would in a lazy language, then skew heaps and many other amortised data structures can be made persistent without damaging their time bounds. We actually need one more property from lazy evaluation to achieve this goal—once a delayed merge has been executed, the resulting tree must be saved so that the delayed merge need never be exe- cuted again, even if the result is needed in different parts of the computation. Fortunately, Haskell provides exactly this behaviour. We will prove that a lazy implementation of skew heaps retains its bounds in the next section. In the remainder of this section, we consider an example that illustrates how lazy evaluation can help skew heaps cope with persistence. We begin with a heap with lots of bad nodes, such as those constructed in Exercise 1.5. In particular, assume we begin with the tree 1 7\ 32 7\ 5 4 \ 6 97 /N\. 99 98 Call this tree T. Now, assume we construct 100 new trees, Tigo... Tigo, each T; the result of inserting i into T. We now have 100 trees of the form 1 7™\. 2 3 /\ 45 / 6 97 /N\ 98 99 \ Each insertion takes O(n) steps, so all the insertions together take O(n?) steps. Or, at least, that would be the time required in a strict language. Using lazy evaluation, each insertion still takes O(n) steps, but only one of those steps is executed immediately. In other words, inserting i produces the tree 14 The Fun of Programming 7 \. 7A 3 i /\ 54 \ 6 97 /\ 99 98 where @ represents the postponed merge. It is not a node of the tree; rather, it is a potential node. It will not become a real node until some other operation is executed that needs information from that node. For example, if we call deleteMin on T;, we get 97 99 98 When we delete the 1, we must compare 2 with the label of its sibling to determine which will become the new root. To answer that, we need the label of the sibling node, which means we need the actual node. Therefore, we execute the delayed computation, producing the node containing 3. However, note that we did not execute all the remaining steps of the insertion. Instead we executed just enough to produce the information we needed at the moment, and no more. In this case, we executed only the next step of the insertion, and re-postponed all the remaining steps. It would take another two deletions to execute the merge step involving 5, two more to execute the merge step involving 7, and so on. In fact, then, the 100 insertions that created Ty99 ... Tig ran in only O(n) time, because each executed only a single step. The potential for the remaining steps is still there, but realising that potential would require roughly O(n) deletions, about 100 for each T;. By shifting the charge for the excess steps from the original insertions to these deletions, we can keep the amortised cost of each operation low. Exercise 1.7 Although the example considered in this section initially ap- peared to be expensive, it actually turned out to be quite cheap, once we accounted for lazy evaluation. Come up with a sequence of n priority queue 1 Fun with binary heap trees 15 operations that takes @(nlog n) time even after lazy evaluation is taken into account. 0 1.7. Analysis of lazy skew heaps The early bird gets the worm but the second mouse gets the cheese. ~ Anonymous The proof that lazy skew heaps support all operations in O(log n) amortised time is very similar to the proof in Section 1.5, but in terms of debits [102] rather than credits. Each debit accounts for a constant amount of work that has been delayed by lazy evaluation, and must be discharged before the corre- sponding work can be executed. We will also assign debits to work that is not delayed, but those debits must be discharged immediately. If we can prove that no work is ever executed before the corresponding debit has been discharged, then we know that our total running time is bounded by the total number of debits discharged, and we claim that each operation runs in amortised time proportional to the number of debits discharged by that operation. In the presence of persistence, we may accidentally discharge a debit more than once. Fortunately, this does no harm. We can be sure that the work corresponding to a particular debit will be executed at most once, no matter how many times we discharge it. Therefore, discharging a debit more than once can only result in an overestimate of the running time, which is safe. We shall prove that merge discharges O(log n) debits, from which we conclude that insert and deleteMin discharge O(log n) debits as well. As in Section 1.5, define a node to be good if its right subtree is at least as big as its left, and bad if its left subtree is bigger than its right. For the purposes of this definition, we count all logical nodes of a tree, even if some of them have not yet been physically constructed because of lazy evaluation. Good nodes may carry a single debit, but bad nodes must be free of outstanding debits. Consider a merge of two trees T; and T> of sizes m, and mp, respectively, and let n= m, + mp. We will show that such a merge discharges O(log m, + log mz) = O(log n) debits. Suppose that there are a total of k + 1 merge steps, the first k of which invoke the join function. Join (Fork x. ab) c = Fork x b (merge ac) There are two cases for each of the k join steps: « If Forkxab is bad, then the resulting node is guaranteed to be good. We attach the debit for this join step to the resulting node, but do not discharge it. 16 The Fun of Programming « If Fork x ab is good, then the resulting node may be bad or good. We discharge at most two debits: the existing one attached to the original good node, and the one for this join step in case the resulting node is bad. In addition, we discharge up to two debits for the terminal merge step: one for the step itself, and one if the non-null node is good. Altogether, we discharge at most 29+2 debits for the entire merge, where gis the number of good nodes encountered during the merge. As in Section 1.5, the definition of good nodes guarantees that g < log rm + log mp, so merge runs in O(log n) amortised time. Exercise 1.8 Re-implement skew heaps using ternary trees rather than binary trees. In other words, add a third slot to the miniature FIFO queue in each node. Adapt the proof that skew heaps run in O(logn) time to this new implementation. 0 1.8 Chapter notes See {99, 101] for more information on debit-based amortised analysis, and {102] for a more comprehensive look at functional data structures. The analy- sis of lazy skew heaps initially appeared in (47] (written in Spanish). Maxiphobic heaps are new, and are intended as a replacement for leftist heaps [29, 80, 22]. The code for maxiphobic heaps is slightly messier, but the analysis is simpler. Round-robin heaps are also new, but are merely a pedagogical stepping-stone on the path to skew heaps. Both maxiphobic and skew heaps make good general-purpose implementations of priority queues. Specification-based testing with QuickCheck Koen Claessen and John Hughes 2.1 Introduction How do you know your programs are correct? How do you know programs you receive from other people are correct? In this chapter, we shall describe a tool which, without giving any absolute guarantees, can improve your confidence in both your own and other people's code. Before we can even discuss the correctness of a function, we must know what it is supposed to do — we must know its specification. Often we have only an informal description in a comment, or in separate documentation. But informal descriptions are notoriously open to interpretation. Far better is a formal, unambiguous specification, stating precisely when the function is applicable and what its results should be. Just formulating specifications precisely is invaluable as a tool for understanding programs. Yet the most precise specification is worthless unless the program actually implements it. The only certain way to establish that is by formal proof, but program proofs are too expensive at present to be used on a large scale. The alternative is testing. Fortunately, despite Dijkstra’s remark that testing can never show the absence of errors, only their presence, it is rather good at the latter. But ad hoc manual testing, with results examined by eye, is of somewhat unquantifiable value. To have confidence in the results of testing, you need a record of what was tested, how thoroughly, and how success was defined. Moreover, the testing should be repeatable, and indeed repeated whenever the source code changes. QuickCheck is a testing tool for Haskell that defines a formal specifica- tion language used to write properties directly in the source code. QuickCheck also defines a test data generation language, which permits compact descrip- tion of a large number of tests. Finally, there is a tool which tests all the properties in a module, reporting any failures. Given a program developed using QuickCheck, you can read the properties as a specification with some confidence, knowing that the program has been tested against them, and that you can repeat the tests at will. Both languages QuickCheck defines are domain-specific embedded lan- 18 The Fun of Programming guages, meaning they are implemented as combinators defined in Haskell. This means they will be largely familiar to you already, and that you can use the full power of Haskell in properties and test data generators. Indeed, they are just defined in module QuickCheck, which is distributed along with Hugs and GHC. We will not describe the implementation of QuickCheck here, but full details can be found in [23]. The chapter is organised as follows: Section 2.2 introduces the concept of testable properties and how to write them in QuickCheck. Section 2.3, as an example, uses properties in the development of an implementation of an abstract datatype for queues. Section 2.4 explains how to define custom test data generators. Section 2.5 shows how to monitor the test coverage of a property under test. Section 2.6 discusses a larger case study: a propositional logic theorem prover. Section 2.7 concludes. 2.2 Properties in QuickCheck Let us begin by introducing QuickCheck’s property language. In simple cases, properties are defined as functions with a result of type Bool. For example, the associativity of addition would be expressed as prop-PlusAssociativex yz = (x+y) +z = x+(¥+2z) ‘The function parameters x, y and z are implicitly universally quantified. The property can be tested by loading the module into GHCi or Hugs, and calling quickCheck prop-PlusAssociative which calls the property with 100 random sets of arguments. Alternatively, a small program (also called quickCheck) can be run from the command line to test all the properties in a module at once: $ quickCheck Module.hs We give properties names beginning with prop. to enable this program to find them. This particular property cannot be tested yet, because it is overloaded: attempting to do so produces an error message reporting ambiguous over- loading, If we add a type signature, prop-PlusAssociative :: Integer ~ Integer ~ Integer ~ Bool then testing can go ahead: Main) quickCheck prop_PlusAssociative OK, passed 100 tests. 2. Specification-based testing with QuickCheck 19 type Queue a = [a] empty ] add xq =qt [x] isEmpty q all q front (x: q) remove (x: q) Figure 2.1: An abstract model of queues. We must specify a type so that QuickCheck can tell what kind of test data to generate, but note also that the type can affect the result. If we specify Float rather than Integer arguments, testing produces Main) quickCheck prop PlusAssociative Falsifiable, after 13 tests: 1.0 -5.16667 ~3.71429 since floating point addition is not associative (because of rounding errors). When a test fails, the values of the property arguments are displayed, along with the number of previously successful tests. As another example, suppose we are testing insertion into ordered lists. We cannot simply specify prop InsertOrdered x xs = ordered (insert x xs) because this is too strong: the result of insert will be ordered only if xs already was. We can capture this by adding a precondition to the property: proptnsertOrdered —:: Integer — [Integer] ~ Property prop_InsertOrdered x xs = ordered xs => ordered (insert x xs) (where => is ==> in ASCII). Testing discards test cases which do not satisfy the precondition, so we test the conclusion with 100 ordered lists. Notice that the result type is different in this case: = isn't a simple boolean operator since it affects the selection of test cases. All such operators in QuickCheck have the result type Property. Alternatively, rather than quantify over all lists and then select the ordered ones, we can quantify explicitly over ordered ones: prop_InsertOrdered :: Integer + Property prop_InsertOrdered x = forAll orderedLists $ \ xs + ordered (insert x xs) 20 The Fun of Programming type Queuel « == ([a], []) empty (tL. 0) addl x (f,b) (f.x:b) isEmpty! (f,b) = null f frontl (x:f,b) =x removel (x: f,b) = flipQ (f,b) where flipQ ({], b) = (reverse b,[}) fipQq q Figure 2.2: An efficient implementation of queues. Here we quantify xs explicitly, since it ranges only over a subset of the type of lists, but leave the quantification of x implicit since it ranges over the entire set of Integers. Replacing preconditions by explicit quantification makes for more efficient testing, and has other advantages also (see Section 2.5). Exercise 2.1 The specification of insert is incomplete, because it permits any ordered list as the result. Write a property which also requires the result to have the same elements as x: xs. 0 Exercise 2.2 Program, specify, and test merge, which merges two ordered lists, and use it to implement and test merge sort. 0 2.3 Example: Developing an abstract data type of queues Specifying queues To illustrate the use of properties, we will develop an efficient implementation of First-In-First-Out queues. We begin by specifying queues, which we do via an abstract model (see Figure 2.1). This is an executable specification, and captures clearly how queues should behave, but it is not an efficient implementation because the cost of add is proportional to the number of elements already in the queue. A more efficient queue implementation Lists are a poor representation for queues, because although removing ele- ments from the front is cheap, adding elements at the rear is expensive. This suggests we might implement a queue by a pair of lists instead, the front and the back, with the back elements held in reverse order. This representation provides amortised constant time access to both the front and the back of 2. Specification-based testing with QuickCheck 2 prop.empty = retrieve emptyl prop.add x q prop.isEmpty q prop-front q Prop-remove q empty retrieve (addl xq) = add x (retrieve q) = isEmptyl q = isEmpty (retrieve q) frontl q = front (retrieve q) retrieve (removel q) == remove (retrieve q) Figure 2.3: Correctness of the queue implementation (first attempt). the queue, provided we are careful to ‘flip the queue’ when the front becomes empty — see Figure 2.2. (The amortised cost of each operation is constant, pro- vided the queue is never used persistently, even though occasionally remove takes more than constant time. Okasaki describes this and a number of other queue implementations [100]. See also Chapter 1 for more on amortisation and persistence.) The same abstract queue may have many different representations in the implementation, but each Queuel can be interpreted unambiguously as, a Queue. We can formalise this interpretation by defining a so-called ‘retrieve’ function, which recovers the abstract value from its representation: retrieve (f,b) = f + reverseb Since we plan to test our implementation with queues of integers, we restrict the type of retrieve to this case: retrieve :: Queuel Integer ~ [Integer] With this restriction, we will not need to write explicit type signatures on each property. Correctness properties We are now in a position to state what it means for the efficient queue oper- ations to be correct: we can use the retrieve function to map their arguments and results to abstract queues, and thus check that they deliver the same results as the abstract operations. The correctness properties are stated in Figure 2.3. Are these properties satisfied? We invoke quickCheck, and see Main) quickCheck prop.isEmpty Falsifiable, after 4 tests: (C1,{-1) 22 The Fun of Programming prop.empty = retrieve empty! prop.addxq = invariant q => retrieve (addl xq) = add x (retrieve q) prop.isEmpty q = invariant q => isEmptyl q == isEmpty (retrieve 4) prop.frontq — = invariant q & not (isEmptyl q) => frontl q = front (retrieve q) prop.remove q = invariant q && not (isEmpty! q) => retrieve (removel q) == remove (retrieve q) == empty Figure 2.4: Correctness of the queue implementation (corrected version). Of course! When we defined isEmptyl (and also front! and removel), we as- sumed an invariant on the queue representation: that the front of a queue will only be empty if the back also is. Although we took the invariant into account when we defined removel, we have not made it explicit in the properties. We define invariant (f,b) = not (null f) || null b and add a precondition invariant q => to each property. Once again, we shall restrict the type of invariant invariant :: Queuel Integer ~ Bool so that later properties which use it can be written without type signatures. We test again. This time prop_isEmpty is checked without mishap, but when we test prop_front we encounter a run-time error: Program error : frontl ({},{]) Of course, front! and removel should not be called with empty queues — but we forgot to state that in their specifications! We add not (isEmptyl q) to their preconditions, and test again. This time, all properties test successfully. But we are not yet done. We have formalised an invariant on queue repre- sentations, and checked that queue operations behave correctly on represen- tations that satisfy the invariant — but we have not checked that the queue operations produce queues satisfying the invariant! We write a property for each operation that produces a queue — see Figure 2.5. Checking these properties reveals another error: Main) quickCheck prop.inv.add Falsifiable, after 0 tests: 0 (1,0) 2. Specification-based testing with QuickCheck 23 prop.inv.empty = invariant emptyl prop.inv.addxq = invariant q => invariant (addl x q) prop.inv.remove q = invariant q 8&& not (isEmptyl q) = invariant (removel q) Figure 2.5: The queue invariant is preserved. Of course, we must ensure the invariant holds not only after remove, but also after adding an element to an empty queue: adding to the back of the queue breaks the invariant in this case! We correct the definition of addI to addI x(f,b) = flipQ (f,x:b) and now all the properties check successfully. The experience from this example is fairly typical. We found only one bug in the implementation under test, but on the other hand we found both missing preconditions and a missing invariant in the properties we first wrote down. Those conditions have now been made explicit and documented in the source code, in the corrected property definitions. They represent both better understanding of the code on our part and valuable documentation for later users of this module. Exercise 2.3 An editor buffer can be modelled by a string and a cursor posi- tion: type Buffer = (Int, String) Define operations empty =: Buffer — the empty buffer insert :: Char — Buffer + Buffer — insert character before cursor delete :: Buffer ~ Buffer — delete character before cursor left: Buffer ~ Buffer — move cursor left one character right :: Buffer ~ Buffer — move cursor right one character atLeft =: Buffer ~ Bool — is cursor at left end? atRight :: Buffer ~ Bool — is cursor at right end? These will serve as the specification of buffer operations. A more efficient way to represent a buffer is as a pair of strings, type Buffer! = (String, String) where the first string contains the characters before the cursor, in reverse or- der, and the second string contains the characters after the cursor. Implement 24 The Fun of Programming prop.isEmpty q prop.front_empty x prop-front_add x q invariant q => isEmptyl q rontl (addl x emptyl) = x invariant q & not(isEmptyl q) => front! (add! x q) =: frontl q prop.remove.empty x = removel (add x emptyl) == empty! prop.remove.add xq = invariant q & not(isEmptyl q) => removel (addl xq) = addl x (removel q) (q= empty!) Figure 2.6: An algebraic specification of queues. the buffer operations on this new type. Define a retrieve function from Buffer! to Buffer, and use it to write QuickCheck properties which specify the correctness of the second imple- mentation with respect to the first. 0 Algebraic specification An abstract model is not the only kind of specification we might wish to use. One common alternative is an algebraic specification, in which we give equa- tions which operations ought to satisfy. It is equally simple to capture such a specification using QuickCheck: an algebraic specification of queues appears in Figure 2.6. (Of course, we can only require that the equations hold for queues which satisfy the representation invariant). These equations provide a complete specification of the queue operations: they enable any queue to be expressed in the form addl x; (addl xp ... (add xz empty!)) and determine the value of each observation. But when we check them, we discover that prop.remove-add fails! Main) quickCheck prop.remove.add Falsifiable, after 1 tests: 0 (11, [0}) Evaluating the left-hand side of the equation yields ({0, 0), { }), while the right hand side yields ({0], {0]) — and while these represent the same queue, they are not equal! Of course, the equations in an algebraic specification cannot be interpreted to mean that the representations in the implementation are exactly equal, only that they are equivalent. So we must define this notion, which we can do conveniently using the function retrieve from the previous section: 2. Specification-based testing with QuickCheck 25 q ‘equiv’ q’ = invariant q & invariant q’ & retrieve q = retrieve q When we replace equalities between queues by ‘equiv’, we find that the prop- erties hold. However, we are not yet done: we must show that each queue operation produces equivalent results from equivalent arguments. For example, for the addl operation we formulate the property prop.add.equiv qq x = q ‘equiv’ q’ => addi xq ‘equiv’ addi xq’ But when we try to check this property, we find testing is very slow, and ends with the message Main) quickCheck prop.add.equiv Arguments exhausted after 58 tests. The problem is that, while the formulation of this property is perfectly valid mathematically, it is not suitable for testing. Since we quantify over all queues q and q’, we generate test cases by choosing two random queue representa- tions, and discarding the case if they are not equivalent. Clearly, the vast ma- jority of test cases will be discarded — hence testing proceeds slowly. There is a limit on the total number of attempts, and once this is reached then QuickCheck gives up, and reports that only 58 test cases could be found. To address this problem, we must learn about QuickCheck’s support for custom test data generation. Exercise 2.4 Construct an algebraic specification of an editor buffer (see Exer- cise 2.3), by formulating equations you expect to hold, and using QuickCheck to test them. (This will be easier if you use the Buffer! type to test your equa- tions — can you see why?) Aim to find equations which allow you to express any buffer using only empty, insert and left. 6 2.4 Quantifying over subsets of types Explicit quantification The parameters of QuickCheck properties are, by default, quantified over all elements of the right type. Often, as we have seen, we want instead to quantify over a subset of these elements. So far, we have represented these subsets by boolean functions, used as preconditions which select test cases. This works well when most elements of the type are in the subset, but poorly when few are. QuickCheck offers another way to represent a (non-empty) set: as a gen- erator for its elements. A generator of type Gen « can be used to generate a random element of type a;; we can think of it as representing the set of values which can be generated. Generators are used together with the forall function 26 The Fun of Programming to quantify over the elements of the set. The property for.ll set p is tested by generating random elements of the set, and testing the property p for each of them. A relation, such as equiv above, can thus be represented in several dif- ferent ways in QuickCheck. One possibility is as a boolean valued function: this makes it easy to test whether two values are related, but hard to generate related elements. Another possibility is as a function from one value to a set of (that is, generator for) related values: this makes it easy to generate related elements, but hard to test whether two values are related. Let us thus define an alternative representation of the equivalence relation on queues as equivQ :: Queuel « - Gen (Queuel x) We'll see the actual definition below. But for now, note that we have two representations of the same relation, and of course, they must agree. We can check that generated elements really are related: prop.equivQ q = invariant q => forAlll (equivQ q) $ Aq’ ~ q ‘equiv’ q’ (Recall that the $ operator is just function application, used here to avoid a need for brackets around the A-expression). However, the dual property, that all related elements can be generated, cannot in general be established by testing. We can now restate the property that add! maps equivalent queues to equivalent queues as prop.add.equiv qx = invariant q => forall (equivQ q) $ Aq’ ~ addl xq ‘equiv’ addl xq’ This property, and similar properties for front and remove, can now be tested quickly. Defining generators Defining generators is eased by the fact that the type Gen is a monad: returna always generates a, and so represents the set {a}, and do {x — s; e} can be thought of as {e | x © s}. Other monad operators from Haskell's standard Monad library can also be used with good effect. The most basic function that makes a choice is which generates a random element of the given range. For example, choose (1, n) represents the set {1...n}. We can use this to define equivQ 4: 2. Specification-based testing with QuickCheck 27 equivQ q = dok — choose (0,0 ‘max‘ (n-1)) return (take (n ~ k) els, reverse (drop (n ~ k) els)) where els = retrieve q n = lengthels This generates a random queue with the same elements as g. We choose the number k of elements in the back of the queue to be strictly less than the total number of elements n, unless of course nis zero. Type-based generation is performed by the overloaded generator arbitrary. Itis used, among other things, to generate the arguments of properties. Thus, for example, prop.max.lexy = x < xX ‘max' y is equivalent to prop.max.le = forAll arbitrary § Ax — forAll arbitrary $ Ay ~ xX Arbitrary(OrderedList «) where arbitrary = liftM OL orderedLists If we redefine insert with the type insert : Ord « > a — OrderedList « + OrderedList o then arguments generated for it will automatically be ordered. QuickCheck encourages a more careful use of types than usual, where values used for different purposes are given different types, so that they may in turn be given different default test data generators. Of course, it isn’t necessary to do this: one can always supply an appropriate explicit generator in each case. But to do so is error-prone, since it is easy to use an inappropriate generator by mistake, which sometimes leads to meaningless test results. By introducing new types, as we did here, we use Haskell's type checker to ensure that we supply the right test data generator in each case. Controlling the size of test data Type-based test data generation can sometimes produce unreasonably large results. For example, in one run when 100 lists of integers were generated, the average length was about 6 elements, and the longest generated list was 38 elements. These are acceptable sizes for test data in most cases. But when we generated a list of lists instead, the average total number of elements was 66, and the largest total number was 928! Clearly, we should reduce the average length of each generated list, so that the total number of elements remains small enough for a fast test. 2. Specification-based testing with QuickCheck 29 QuickCheck generators are parameterised on an integer size, which is grad- ually increased during testing. Thus, the first tests explore small cases thor- oughly, while the later cases grow larger and larger. The interpretation of the size parameter is up to the implementor of each test data generator, however. For example, the default generator for lists interprets it as an upper bound on the length. To define a generator that depends on the size, we use the function sized :: (Int ~ Gena) ~ Geno For example, the default list generator is defined as sized $ An ~ do len — choose (0, n) vector len where vector len generates a list of random values of length len: vector n = sequence arbitrary | i — (1..n}] To control the size of generated values we can use resize :: Int ~ Gena ~ Gen which supplies an explicit size parameter to a generator. For example, to generate a list of lists and bound the total number of elements by the size, we can use the default generator but replace the size parameter by its square root. Such a generator can be written as sized § An — resize (round (sqrt (fromInt n))) arbitrary where the list of lists is generated by the default generator arbitrary, but with asmaller size parameter. User-defined types While QuickCheck defines test data generators for all of the built-in types of Haskell, it cannot do so for user-defined types. These must be provided by the user, by defining a suitable instance of class Arbitrary. For recursive types, it is important to control the size of the result. For example, suppose we want a test data generator for the type of binary trees data Tree « = Leaf | Branch (Tree x) « (Tree «) Itis tempting to define a generator more or less as follows: instance Arbitrary « > Arbitrary (Tree «) where arbitrary = frequency (1, return Leaf), (3, liftM3 Branch arbitrary arbitrary arbitrary) | 30 The Fun of Programming where we choose a Leaf less often than a Branch to avoid the majority of generated trees being trivial. Unfortunately, the probability of getting a finite tree from this generator is only 33%! To understand why, notice that once we have chosen a Branch, generation terminates only if both subtrees are finite; once we have generated a few levels, we can terminate only if very many subtrees are finite, This is not very likely. To guarantee termination (and to keep test data a reasonable size), we use the size parameter to bound the generated tree. instance Arbitrary « > Arbitrary (Tree «) where arbitrary = sized arbTree arbTree0 = return Leaf arbTreen | n>0 = frequency (1, return Leaf), (3, liftM3 Branch shrub arbitrary shrub) | where shrub = arbTree (n‘div' 2) Notice that shrub here is a generator for smaller trees; it is not itself bound toa particular smaller tree. Thus we can use it twice, and generate a different tree at each use. Since each Branch contains two subtrees, we halve the size bound for each one, and thus bound the overall number of nodes by the size. This technique is necessary in generators for most recursive types. 2.5 Test coverage In the previous sections we have seen how to formulate properties and test them. What values the properties are tested on, however, has not so far con- cerned us. But in many cases it can be useful to monitor the test data that we have tested the property on, so as to get an indication of the test coverage of a particular QuickCheck test run. For example, let us take the property prop-InsertOrdered, defined in Sec- tion 2.2: prop_InsertOrdered :: Integer — [Integer] ~ Property prop_InsertOrdered xxs = ordered xs => ordered (insert x xs) To test this property, QuickCheck generates arbitrary lists, filters out the or- dered ones, and checks if the insert function works correctly on those. But what is the probability that a randomly generated list is ordered? It depends on the length of the list: the shorter the list, the larger this probability be- comes. Thus there is a risk that the implication => skews the test cases, so that mostly very short lists are tested! This is of course unacceptable. To check what proportion of the test cases are trivial (in some user- definable sense), we use the trivial combinator from the QuickCheck library, as follows: 2. Specification-based testing with QuickCheck 31 prop-InsertOrdered :: Integer ~ [Integer] ~ Property prop_InsertOrdered x xs = ordered xs => trivial (length xs < 2) $ ordered (insert x xs) Here, we classify the test cases with a length two or less as trivial. When we now test the property, we get the following behaviour: Main) quickCheck prop_InsertOrdered OK, passed 100 tests (91% trivial). This is alarming! 91 out of 100 test cases used a list of length two or less. This is not enough to test the insert function, since it is quite easy to write such a function which works for very short lists, but not for longer ones. We can see that we should be very careful, when using the implication operator(—), that we do not skew the test cases too much. In this case, it is much better to use a custom-made generator for ordered lists, as discussed later on in Section 2.2. The trivial combinator is actually an instance of a slightly more general combinator called classify: trivial p = classify p “trivial” To get more information, we can use the function classify multiple times: prop_InsertOrdered x xs = ordered xs => classify (null xs) “empty lists” $ classify (length xs ++ 1) “unit lists” $ ordered (insert x xs) Testing this property gives: Main) quickCheck prop_InsertOrdered OK, passed 100 tests. 42% unit lists. 40% empty lists. There is also a way to collect values over all test cases, using the combinator collect: prop_InsertOrdered x xs = ordered xs => collect (length xs) $ ordered (insert x xs) Testing now results in: Main) quickCheck prop _InsertOrdered OK, passed 100 tests. 46% 0. 34% 1. 15% 2. 5% 3. 32 The Fun of Programming Exercise 2.5 Check that the properties used in the Queue example in Sec- tion 2.3 do not skew the test data unacceptably, by adding appropriate test data monitors. 0 2.6 A larger case study As a larger case study, let us develop a simple theorem prover for classical propositional logic. A formula in propositional logic consists of a number of variables that can assume boolean values true and false, put together using connectives. For simplicity, we assume that we only have two connectives: conjunction (‘and’), and negation (‘not’). We know that any other logical con- nective, such as disjunction (‘or’) can be expressed in terms of those. Datatypes for propositional logic First, we declare a new type of names, that we will use for variables. newtype Name = Name String deriving (Eq, Show) Then, we define a datatype for formulas. data Form = Var Name — variable | Form :& Form — conjunction | Not Form — negation deriving (Eq, Show) One function on formulas that we will use often is the function that gathers all variables used in a given formula: names :: Form ~ [Name] names (Varv) = [v] names (p :& q) = names p ‘union’ names q names (Not p) = names p Here, we use the standard Haskell function union, which appends two lists, removing duplicate elements from the result. A valuation is a mapping from variable names to true or false. We repre- sent such a mapping using a list of variable names that map to false, and a list of variable names that map to true. Naturally, these two lists must be disjoint. ‘This representation has the advantage that it is an explicit representation, so that printing it is easy, and it also gives us the possibility of representing partial valuations. A partial valuation leaves some variables undefined. [Name] } data Valuation = Val { falses :: [Name], trues deriving (Eq, Show) 2 Specification-based testing with QuickCheck 33 A model of a formula is a valuation that, when substituting corresponding values for variables, makes the formula evaluate to true. In order to implement our theorem prover, we will use a function that, given a formula, returns all models of that formula. Then, in order to prove that a given formula p is valid, we just check that the negation of p has no models. Fi 9 models of formulas Finding models of a formula can be done in many ways. Here, we will use the a version of the well-known tableau method. The idea is to propagate knowledge about the values of variables. This knowledge is represented using (partial) valuations. In the beginning, we know nothing: nothing :: Valuation nothing = Val { falses = (J, trues = (]} Then, we look at the formula to see how to gather knowledge. Given a formula which must be true, and partial knowledge about the variables in it, we can produce a list of possible models of that formula. tableau :: Form ~ Valuation ~ [Valuation] Let us look at the easy cases first. If the formula is a variable Var v, then v must be true. If we already know that v is true, we just produce the information we already had. If we have the information that v should be false, then there can not be any model. Otherwise, we produce the current information, but we add the fact that v must be true. tableau ( Var v) val | v ‘elem’ trues val | v ‘elem* falses val | otherwise [val] 0 (val{trues = v: trues val}) In a similar fashion, if the formula is a negated variable Not (Var v), we act accordingly. tableau (Not (Var v)) val | v ‘elem' trues val = [ | v ‘elem’ falses val = [val] | otherwise = [val{falses = v: falses val} } In the case where the formula is a conjunction p :& q, the models are exactly those which are models for p and models for g. So, we first propagate the current knowledge through p, and then the resulting knowledge through 4. tableau (p :& q) val = [ Valpg | valy — tableau p val, valg — tableau q valy | 34 The Fun of Programming Lastly, when finding models for the negation of a conjunction Not (p:& q), we are interested in models which either make Not p true or Not q true. In order words, we will search for models of both kinds, and combine the results. tableau (Not (p :& q)) val = tableau (Not p) val + tableau (Not q) val Given the tableau algorithm, we can now define a function which returns all models of a formula, by starting the tableau with no knowledge. models :: Form ~ { Valuation] models p = tableau p nothing However, the list of models can also contain partial valuations. A partial valuation in the result list represents a set of models; whatever value we take for the variables that are undefined, it will still be a model. This distinction becomes important later. Arbitrary instances Before we can check properties of the presented implementation of models, we first need to define generators for the types we are going to quantify over. First, let us take a look at the type Name. When generating an arbitrary name, we should not just generate an arbitrary string. If we did that, the probability of two arbitrary names being equal would be too low to be of practical value. Instead, we will let the size parameter of the generator decide how large the set of names is that we pick an arbitrary name from. instance Arbitrary Name where arbitrary = sized $ An ~ do c ~ elements (take(n + 1) ('a' .. '2')) return (Name [c}) Here, we use the combinator elements, which picks a random element from a given list. Next, we should decide how to generate arbitrary valuations. The problem is that we are hardly ever interested in completely arbitrary valuations. Mostly, we want to create arbitrary valuations over a given list of names. We therefore decide to make our own custom generator, rather than use the Arbitrary class. valuationOver :: valuationOver ns do bs — vector (length ns) return (Val {falses = [n | (n,False) ~ ns ‘zip' bs}, trues = [n | (n, True) — ns ‘zip' bs }}) {Name] — Gen Valuation We create an arbitrary vector of booleans, which we use to decide whether the given name should be put in the list of false or true names. 2. Specification-based testing with QuickCheck 35 Lastly, we should define how to generate arbitrary formulas. Since we already know how to generate arbitrary names, the problem is very similar to generating arbitrary binary trees, presented in Section 2.4. Exercise 2.6 Make the Form datatype an instance of the class Arbitrary. Pick a reasonable probability distribution for the constructors, and do not forget to take care of the size argument! Check, using collect, that your generator has the intended behaviour. 0 Checking soundness The basic function we want to check properties of is the models function. There are two standard properties we would like to check: soundness: ‘Are all the models that are found really models of the formula’, and completeness: ‘Does the algorithm find all models of the formula”. Let us first take a look at how we can formulate the soundness property. Firstly, we need to define a function that checks if a given valuation is a model of a given formula or not. We decide to borrow notation from logic, and use m = p, read as m satisfies p. The = operator can easily be defined recursively in Haskell as follows: (&) :: Valuation — Form ~ Bool val = Varv | v ‘elem’ truesv = True | v ‘elem’ falses v = False | otherwise = error “undefined variable” val - (p :& q) = (val = p) & (val © q) val = Not p = not (val = p) We can see that we have to be careful with this operation. Checking whether a given valuation satisfies a given formula can in the general case only be done if the valuation is defined for all variables occurring in the formula. Applying the = operator to a partial valuation can therefore lead to an error in this function. A first try to express the soundness property might take the following form: prop.Sound :: Form ~ Bool prop-Sound p = all(Aval ~ val = p) (models p) This can be read as: for any formula p, every valuation of p found by the models algorithm satisfies p. However, this property does not hold, as evaluating quickCheck prop-Sound shows: Program error: “undefined variable” The models function sometimes produces partial valuations, and rightly so! However, the function expects only totally defined valuations. We can get 36 The Fun of Programming around this problem by defining a function totalise that takes two valuations and extends the first valuation with values from the second valuation if the first valuation does not define that value. totalise :: Valuation ~ Valuation ~ Valuation val ‘totalise' ext = Val { falses = falses val + (falses ext \\ defined), trues = trues val + (trues ext \ defined) } where defined = falses val + trues val ‘Now, we can extend a partial valuation with another total valuation, in order to check whether a partial model really is a model. We can thus adapt the definition of the soundness property: prop.Sound :: Form ~ Property prop.Sound p = forAll (valuationOver (names p)) $ Aext + all (Aval + (val ‘totalise' ext) = p) (models p) This property expresses the right thing! However, when we run it through QuickCheck, we get: Program error: tableau (Not (Not (...)) (...) This time the error indicates that we have not defined the tableau function fully. In particular, we have forgotten to define what the answer is when using a double negation, We have thus found a bug! Fixing this bug is rather easy, since double negation is the identity on formulas. We add the following case to the definition of the tableau function: tableau (Not (Not p)) val = tableau p val And we check the soundness property again. This time, it passes through QuickCheck without problems. Checking completeness The next property of our model finding algorithm we should check is com- pleteness — do we really find all models? In order to check this property, we need to realise that the list of results from the models function might contain partial valuations, which themselves represent sets of models. To check that a list of such partial valuations is complete, we need to check that any total valuation satisfying p is covered by at least one of the valuations. One valuation covers another if they agree on the values of the variables they both define, and the first one defines no more variables than the second. We thus define the following function: covers :: Valuation ~ Valuation + Bool val ‘covers' val’ = falses val ‘subset’ falses val’ & trues val ‘subset’ trues val where xs‘subset" ys = alll (‘elem ys) xs 2. Specification-based testing with QuickCheck 37 The completeness property can now be formulated as follows: prop-Complete :: Form ~ Property prop-Complete p = forAll (valuationsOver (names p)) $ Aval + val = p => any(Aval’ — val! ‘covers' val) (models p) We can read this as: ‘For all valuations over the names occurring in p, if the valuation makes p true, then there should be a model in the list returned by models p which covers the valuation.’ Checking quickCheck prop.Complete now results in the comforting message ‘OK, passed 100 tests’. As always when using implication in a property, we should be careful not to skew the test cases too much. A danger might be that the formulas and valuations that pass the condition are mostly trivial. Let us therefore monitor the number of distinct variables that are present in a given formula after it has passed the condition. We modify the property by adding the monitor trivial (length (names p) < 2) right after the implication symbol in the property. Running QuickCheck on this modified property reports that 35% of the test cases are classified as being trivial’. So, we know that almost two thirds of the test cases are non-trivial, which is a soothing thought. Exercise 2.7 Redefine the property prop.Complete in such a way that checking the property produces a more detailed table of model sizes, for example in the following way: Tableau) quickCheck prop.Complete OK, passed 100 tests. 51% 0-4. 28% 5-9. 15% 10-14. 6% 15-19. a Exercise 2.8 As is well-known to logicians, a proposition A => B(‘if Athen B') is logically equivalent to not B => not A (‘if not B, then not 4’). Use this insight torewrite the prop.Complete property to the equivalent ‘For all valuations over the names occurring in p, if there is no model in the list returned by models p which covers the valuation, then the valuation cannot make p true.’ How well does this new formulation of the property perform in tests? 0 Asymmetric branching We have now formulated and tested correctness properties of our model finder. However, it took quite a lot of work to define all the helper functions. In this ‘This result is of course dependent on how you made Form an instance of Arbitrary in Exercise 2.6. 38 The Fun of Programming section we show that this work did not go to waste, because we can reuse a lot of definitions that we have already defined when we are improving the tableau algorithm. Let us study the tableau algorithm a bit more closely, and look at the case Not (p :& q), where the search branches. The current implementation can actually lead to duplication of models in the list returned. The reason for that is that the two cases investigated (Not p and Not q) are not exclusive, so models where both p and q are false will be produced twice. Let us formulate the exclusivity property that we want to hold for our algorithm: ‘There can be no model in the list which is covered by another model in the same list’. prop-Exclusive :: Form - Property prop_Exclusive p not (any [ val ‘covers‘ val’ | val — ms, val’ — ms \\ [val] ]) where ms = models p As we claimed earlier, the exclusivity property does not hold for our current tableau algorithm, as evaluating quickCheck prop_Exclusive reveals: Falsifiable, after 5 tests: Not (Var (Name “b”) :& (Var (Name “b”))) In order to make this property valid for our algorithm, we apply a technique that is called asymmetric branching. Instead of branching on the cases Not p and Not q, which might contain overlapping models, we branch on Not p and p :& Not g, which are guaranteed not to overlap. Here is the change in the code we have to make: tableau (Not (p :& q)) val = tableau (Not p) val + tableau (p : Not q) val It takes a little bit of reasoning to understand that the algorithm is still sound and complete. However, quickCheck strengthens our confidence, since it finds no counterexample for the properties prop.Sound and prop.Complete. Even prop-Exclusive is checked without problems! Looking at the above small change, we might be tempted to make the branching symmetric again, by adding q on the left-hand side, just as we added p on the right-hand side. This small change leads to: tableau (Not (p :& q)) val = tableau (Not p :& q) val + tableau (p :& Not q) val When we now check our three properties again, we see that prop.Sound and prop-Exclusive still hold, but the algorithm is sadly not complete anymore. After running quickCheck prop.Complete, we get: 2. Specification-based testing with QuickCheck 39 Falsifiable, after 7 tests: Not ((Var (Name “a") :& Var (Name“f")) :& Not (Var (Name“c"))) Val { falses = [Name “a”, Name “{"], trues = [Name “c”] } So, the last ‘optimisation’ we made was a wrong decision; the models function applied to the above formula only yields models where at most one of a or f is false, but none where both are false. We can thus see that we benefit a lot from formulating properties at an early stage in the design of an algorithm, and checking that properties still hold after each optimisation. 2.7. Conclusions It is common knowledge that formalising specifications is valuable, even with- out formal proofs of program correctness. Yet, as we have seen, the first attempt to formalise a specification is likely to be incorrect, and the value of a faulty specification is limited. QuickCheck finds such errors very effec- tively, and helps us tighten up specifications, by identifying forgotten precon- ditions and invariants, for example. The specification then becomes a form of computer-checked documentation, with strong evidence (if not an absolute guarantee) of correctness. Moreover, QuickCheck often finds bugs in the programs under test. It offers a short term payoff for the labour of writing specifications: an effective testing system which cuts testing time dramatically, while at the same time testing more thoroughly than traditional hand testing. Random testing may seem naive, compared to intelligent choice of test cases, but there are both theoretical and practical results showing this is not the case. To quote Hamlet [50], ‘By taking 20% more points in a random test, any advantage a partition test might have had is wiped out’. Random test cases are so cheap to generate that we can afford to use a lot of them! We have found it to be important to control the distribution of test cases, though. Random testing is particularly suitable for purely functional programs, since each function can be tested independently. Since the first release of QuickCheck in 2000, it has become quite popular. Ithas been used, among other things, to test a data structure library, to develop a Java pretty printing library, and to develop entries for the International Conference on Functional Programming's Programming Contest in both 2000 and 2001. We hope QuickCheck will encourage you to test more thoroughly, and make wider use of specifications. 2.8 Acknowledgements We would like to thank Daniel Vallstrém for the initial implementation of the tableau algorithm presented in Section 2.6. Origami programming Jeremy Gibbons origami (origa-mi) The Japanese art of making elegant designs using folds in all kinds of paper. (From ori fold + kami paper.) 3.1 Introduction One style of functional programming is based purely on recursive equations. Such equations are easy to explain, and adequate for any computational pur- Pose, but hard to use well as programs get bigger and more complicated. In a sense, recursive equations are the ‘assembly language’ of functional pro- gramming, and direct recursion the goto. As computer scientists discov- ered in the 1960s with structured programming, it is better to identify com- mon patterns of use of such too-powerful tools, and capture these patterns as new constructions and abstractions. In functional programming, in con- trast to imperative programming, we can often express the new constructions as higher-order operations within the language, whereas the move from un- structured to structured programming entailed the development of new lan- guages. ‘There are advantages in expressing programs as instances of common patterns, rather than from first principles — the same advantages as for any kind of abstraction. Essentially, one can discover general properties of the abstraction once and for all, and infer those properties of the specific instances for free. These properties may be theorems, design idioms, implementations, optimisations, and so on. In this chapter we will look at folds and unfolds as abstractions. In a pre- cise technical sense, folds and unfolds are the natural patterns of computation over recursive datatypes; unfolds generate data structures and folds consume them. Functional programmers are very familiar with the foldr function on lists, and its directional dual foldl; they are gradually coming to terms with the generalisation to folds on other datatypes (IFPH §3.3, §6.1.3, §6.4). The 42 The Fun of Programming computational duals, unfolds, are still rather unfamiliar (45); we hope to show here that they are no more complicated than, and just as useful as, folds, and to promote a style of programming based on these and similar recursion patterns. We explore folds and unfolds on lists, numbers and trees. In fact, a single generic definition of fold can be given once and for all such datatypes, and similarly for unfold. This fact has been promoted in the world of functional programming by Meijer and others {93}; for a tutorial introduction, see [44], or in a different style, {12}. However, such ‘generic origami’ is beyond the scope of this chapter. 3.2. Origami with lists: sorting The most familiar datatype in functional programming is that of lists. In- deed, from one perspective, functional programming is synonymous with list processing, as reflected in the name of the first functional programming lan- guage, LISP. Haskell has a built-in datatype of lists, with a special syntax to aid clarity and brevity. In this chapter, we will not make use of the special privileges enjoyed by lists; we will treat them instead on the same footing as every other datatype, in order to emphasise the commonalities between the different datatypes. We will start our exploration of origami programming with some algo- rithms for sorting lists. As pointed out by Augusteijn [2], a number of sorting algorithms are determined purely by their recursion pattern; in some fortu- nate cases, once the recursion pattern has been fixed, there are essentially no further design decisions to be made. Evidently, therefore, it is important to be familiar with recursion patterns and their properties: this familiarity may lead a programmer straight to an algorithm. Recall (IFPH §4.1.1) that lists may be defined explicitly via the following datatype declaration: data List « = Nil | Cons ot (List «) As a concession to Haskell’s special syntax, we will define a function wrap for constructing singleton lists: wrap wrap x = List « Cons x Nil We also define a function nil for detecting empty lists: nil nil Nil nil (Cons x xs) List « — Bool True False 3 Origami programming 43 Folds for lists The natural fold for lists may be defined as follows: folal. (0 B= B) — B= Lista — B fold f e Nil =e foldl f e (Cons x xs) = f x (fold f e xs) This is equivalent to Haskell's foldr function; the ‘L’ here is for ‘list’, not for ‘left’. The crucial fact about foldl. is the following universal property: h = foldtfe hxs = case xs of Nil ~e Cons y ys +f y(hys) (Recall that Haskell’s case expression matches a value against each of a se- quence of patterns in turn, yielding the right-hand side corresponding to the first successful match.) Exercise 3.1 Using the universal property, prove the fusion law: for strict h, h- fold fe = foldl f' e& (h(fab) = f’a(hb)) a (he = e) Why does the law not hold for non-strict h? Where does the proof break down? o Exercise 3.2 Define as instances of foldl equivalents of the standard prelude functions map, ++ and concat: mapL ::(« ~ B) ~ List — List B appendL :: List = List « ~ List « concatl +: List (List o) + List o o Exercise 3.3 As a corollary of the general fusion law, and using your answer to Exercise 3.2, prove the map fusion law foldL fe - map g = foldL(f - ge One classic application of foldL is the insertion sort algorithm [80, §5.2.1], as discussed in IFPH Exercise 4.5.4 and §5.2.4. This may be defined as follows: 44 The Fun of Programming isort :: Ord x = List x + List « insert Ord a > « + List + List « insert y Nil = wrapy insert y (Cons x xs) ly List « + 0 minimumL (Cons x xs) = fold min x xs deletel. Eqa => x ~ List ~ List deleteL y Nil = Nil deletel y (Cons x xs) lye = xs | otherwise = Cons x (deleteL y xs) Then selection sort is straightforward to define: ssort :: Ord x = List « — List o ssort = unfold’ delmin Exercise 3.10 The case deletel. y (Cons x xs) requires both the tail xs and the result deleteLy xs on that tail, so this function is another paramorphism. Re- define deleteL using paral. O Exercise 3.11 In fact, delmin itself is a paramorphism. Redefine delmin using paral as the only form of recursion, taking care to retain the order of the remainder of the list. There is another sorting algorithm with a very similar form, known as bubble sort (80, §5.2.2]. The overall structure is the same — an unfold to lists — but the body is slightly different. The function bubble has (of course) the same type as delmin, but it does not preserve the relative order of remaining list elements. This relaxation means that it is possible to define bubble as a fold: 3 Origami programming 47 bubble :: Ord « > List « + Maybe (a, List ce) bubble = fold step Nothing where stepx Nothing = Just (x, Nil) step x (Just (y, ys) [x

You might also like