(download for perfect quality) - See how recursive functions and structural induction relate to recursive datatypes.
Follow along as the fold abstraction is introduced and explained.
Watch as folding is used to simplify the definition of recursive functions over recursive datatypes
Part 1 - through the work of Richard Bird and Graham Hutton.
Errata:
slide 7, 11 fib(0) is 0,rather than 1
slide 23: was supposed to be followed by 2-3 slides recapitulating definitions of factorial and fibonacci with and without foldr, plus translation to scala
slide 36: concat not invoked in concat example
slides 48 and 49: unwanted 'm' in definition of sum
throughout: a couple of typographical errors
throughout: several aesthetic imperfections (wrong font, wrong font colour)
Folding Unfolded - Polyglot FP for Fun and Profit - Haskell and Scala
1. See how recursive functions and structural induction relate to recursive datatypes
Follow along as the fold abstraction is introduced and explained
Watch as folding is used to simplify the definition of recursive functions over recursive datatypes
Part 1 - through the work of
Folding Unfolded
Polyglot FP for Fun and Profit
Haskell and Scala
Graham Hutton
@haskellhutt
@philip_schwarzslides by https://www.slideshare.net/pjschwarz
Richard Bird
http://www.cs.ox.ac.uk/people/richard.bird/
2. Richard Bird
@philip_schwarz
This slide deck is almost entirely centered on material from Richard Birdâs fantastic book, Introduction to Functional
Programming using Haskell.
I hope heâll forgive me for relying so heavily on his work, but I couldnât resist using extensive excerpts from his compelling book
to introduce and explain the concept of folding.
http://www.cs.ox.ac.uk/people/richard.bird/
https://en.wikipedia.org/wiki/Richard_Bird_(computer_scientist)
3. 3.1 Natural Numbers
The natural numbers are the numbers 0, 1, 2 and so on, used for counting. The type ðµðð is introduced by the declaration
ðððð ðµðð = ðððð | ðºððð ðµðð
ðµðð is our first example of a recursive datatype declaration. The definition says that Zero is a value of ðµðð, and that Succ ð is a
value of ðµðð whenever ð is. In particular, the constructor ðºððð (short for âsuccessorâ), has type ðµðð â ðµðð. For example, each of
ðððð, ðºððð ðððð, ðºððð (ðºððð ðððð)
is an element of ðµðð. As an element of ðµðð the number 7 would be represented by
ðºððð (ðºððð (ðºððð (ðºððð (ðºððð (ðºððð (ðºððð ðððð))))))
Every natural number is represented by a unique value of ðµðð. On the other hand, not every value of ðµðð represents a well-
defined natural number. In fact ðµðð also contains the values â¥, ðºððð â¥, ðºððð (ðºððð â¥), and so on. These additional values will
be discussed later.
Let us see how to program the basic arithmetic and comparison operations on ðµðð. Addition can be defined by
+ â· ðµðð â ðµðð â ðµðð
ð + ðððð = ð
ð + ðºððð ð = ðºððð ð + ð
This is a recursive definition, defining + by pattern matching on the second argument. Since every element of ðµðð, apart for â¥, is
either ðððð or of the form Succ ð, where ð is an element of ðµðð, the two patterns in the equations for + are disjoint and cover all
numbers apart from â¥.
⊠Richard Bird
4. Here is how ðððð + ðºððð (ðºððð ðððð) would be evaluated:
ðððð + ðºððð (ðºððð ðððð)
= { second equation for +, i.e. ð + ðºððð ð = ðºððð ð + ð }
ðºððð ðððð + ðºððð ðððð
= { second equation for +, i.e. ð + ðºððð ð = ðºððð ð + ð }
ðºððð ðºððð (ðððð + ðððð)
= { first equation for +, i.e. ð + ðððð = ð }
ðºððð (ðºððð ðððð)
âŠit is not a practical proposition to introduce natural numbers through the datatype ðµðð: arithmetic would be just too inefficient.
In particular, calculating ð + n would require (ð + 1) evaluation steps. On the other hand, counting on your fingers is a good way
to understand addition.
Given +, we can define Ã:
(Ã) â· ðµðð â ðµðð â ðµðð
ð Ã ðððð = ðððð
ð à ðºððð ð = ð à ð + ð
Given Ã, we can define exponentiation (â) by
(â) â· ðµðð â ðµðð â ðµðð
ð â ðððð = ðºððð ðððð
ð â ðºððð ð = ð â ð à ð
âŠ
Richard Bird
5. On the next slide we show the definitions of +, Ã,
and â again, and have a go at implementing the
three operations in Scala, together with some tests.
7. The remaining arithmetic operation common to all numbers is subtraction (â). However, subtraction is a partial operation on
natural numbers. The definition is
â â· ðµðð â ðµðð â ðµðð
ð â ðððð = ð
ðºððð ð â ðºððð ð = ð â ð
This definition uses pattern matching on both arguments; taken together, the patterns are disjoint but not exhaustive. For
example,
ðºððð ðððð â ðºððð (ðºððð ðððð)
= { second equation for â, i.e. ðºððð ð â ðºððð ð = ð â ð }
ðððð â ðºððð ðððð
= { case exhaustion }
â¥
The hint âcase exhaustionâ in the last step indicates that no equation for â has a pattern that matches (ðððð â ðºððð ðððð).
More generally, ð â ð = ⥠if ð < ð. The partial nature of subtraction on the natural numbers is the prime motivation for
introducing the integer numbers; over the integers, â is a total operation.
...Finally, here are two more examples of programming with ðµðð. The factorial and Fibonacci functions are defined by
ðððð¡ â· ðµðð â ðµðð
ðððð¡ ðððð = ðºððð ðððð
ðððð¡ ðºððð ð = ðºððð ð à ðððð¡ ð
ððð â· ðµðð â ðµðð
ððð ðððð = ðºððð ðððð
ððð ðºððð ðððð = ðºððð ðððð
ððð ðºððð ðºððð ð = ððð ðºððð ð + ððð ð Richard Bird
8. See the next slide for a Scala
implementation of the â operation.
9. â â· ðµðð â ðµðð â ðµðð
ð â ðððð = ð
ðºððð ð â ðºððð ð = ð â ð
val `(-)`: Nat => Nat => Nat =
m => n => (m,n) match {
case (_,Zero) => m
case (Succ(x),Succ(y)) => x - y
}
; assert(0 - 0 == 0) ; assert(Zero - Zero == Zero)
; assert(1 - 0 == 1) ; assert(Succ(Zero) - Zero == Succ(Zero))
; assert(5 - 3 == 2) ; assert(Succ(Succ(Succ(Succ(Succ(Zero))))) - Succ(Succ(Succ(Zero))) == Succ(Succ(Zero)))
; assert(3 - 5 == -2) ; assert(
Try {
Succ(Succ(Succ(Zero))) - Succ(Succ(Succ(Succ(Succ(Zero)))))
}.toString.startsWith(
"Failure(scala.MatchError: (Zero,Succ(Succ(Zero)))"
)
)
No equation for â has a pattern that matches (ðððð â ðºððð ðððð).
Similarly for (ðððð â ðºððð (ðºððð ðððð)), (ðððð â ðºððð (ðºððð(ðºððð ðððð))), etc.
More generally, ð â ð = ⥠if ð < ð.
ð â ð throws scala.MatchError if ð < ð.
10. @philip_schwarz
On the next slide we show the definitions of ðððð¡,
and ððð again, and have a go at implementing the
two functions in Scala, together with some tests.
12. 3.1.1 Partial numbers
Let us now return to the point about there being extra values in ðµðð. The values
â¥, ðºððð â¥, ðºððð (ðºððð â¥), âŠ
are all different and each is also a member of ðµðð. That they exist is a consequence of three facts:
i. ⥠is an element of ðµðð because every datatype declaration introduces at least one extra value, the undefined value of
the type.
ii. constructor functions of a datatype are assumed to be nonstrict
iii. ðºððð ð is an element of Nat, whenever ð is
To appreciate why these extra values are different from one another, suppose we define ð¢ðððððððð â· ðµðð by the equation
ð¢ðððððððð = ð¢ðððððððð. Then
? ðððð < ð¢ðððððððð
{Interrupted!}
? ðððð < ðºððð ð¢ðððððððð
ððð¢ð
? ðºððð ðððð < ðºððð ð¢ðððððððð
{Interrupted!}
? ðºððð ðððð < ðºððð (ðºððð ð¢ðððððððð)
ððð¢ð
One can interpret the extra values in the following way: ⥠corresponds to the natural number about which there is
absolutely no information; ðºððð ⥠to the natural number about which the only information is that it is greater than ðððð;
ðºððð (ðºððð â¥) to the natural number about which the only information is that it is greater than ðºððð ðððð; and so on.
Richard Bird
13. There is also one further value of ðµðð, namely the âinfiniteâ number:
ðºððð (ðºððð (ðºððð (ðºððð ⊠)))
This number can be defined by
ððððððð¡ðŠ â· ðµðð
ððððððð¡ðŠ = ðºððð ððððððð¡ðŠ
It is different from all the other numbers, because it is the only number ð¥ for which ðºððð ð < ð¥ returns ð»ððð for all finite
numbers ð. In this sense, ððððððð¡ðŠ is the largest element of ðµðð. If we request the value of ððððððð¡ðŠ, then we obtain
? ððððððð¡ðŠ
ðºððð (ðºððð (ðºððð (ðºððð (ðºððð {ðŒðð¡ðððð¢ðð¡ðð!}
The number ððððððð¡ðŠ satisfies other properties, in particular ð + ððððððð¡ðŠ = ððððððð¡ðŠ, for all numbers ð. The dual equation
ððððððð¡ðŠ + ð = ððððððð¡ðŠ holds only for finite numbers ð. We will see how to prove assertions such as these in the next
section.
To summarise this discussion, we can divide the values of ðµðð into three classes:
⢠The finite numbers, those that correspond to well-defined natural numbers.
⢠The partial numbers, â¥, ðºððð â¥, and so on.
⢠The infinite numbers, of which there is just one, namely ððððððð¡ðŠ.
We will see that this classification holds true of all recursive types. There will be the finite elements of the type, the partial
elements, and the infinite elements. Although the infinite natural number is not of much use, the same is not true of the
infinite values of other datatypes.
⊠Richard Bird
14. Note that when in this slide deck we mention the concepts of â¥
and ððððððð¡ðŠ, it is mainly in a Haskell context, as we did in the last
two slides. In particular, we wonât be modelling ⥠and ððððððð¡ðŠ in
any of the Scala code youâll see throughout the deck.
15. 3.2 Induction
In order to reason about the properties of recursively defined functions over a recursive datatype, we can appeal to a principle of
structural induction. In the case of ðµðð, the principle of structural induction can be defined as follows: In order to show that some
property ð(ð) holds for each finite number ð of ðµðð, it is sufficient to show:
Case (ðððð). That ð(ðððð) holds.
Case (ðºððð ð). That if ð(ð) holds, then ð(ðºððð ð) holds also.
Induction is valid for the same reason that recursive definitions are valid: every finite number is either ðððð or of the form
ðºððð ð, where ð is a finite number. If we prove the first case, then we have shown that the property is true for ðððð; If we also
prove the second case, then we have shown that the property is true for ðºððð ðððð, since it is true for ðððð. But now, by the
same argument, it is true for ðºððð ðºððð ðððð , and so on.
The principle needs to be extended if we want to assert that some proposition is true for all elements of ðµðð, but we postpone
discussion of this point for the following section.
As an example, letâs prove that ðððð + ð = ð for all finite numbers ð. Recall that + is defined by
ð + ðððð = ð
ð + ðºððð ð = ðºððð ð + ð
The first equation asserts that ðððð is a right unit of +. In general, ð is a left unit of â if ð â ð¥ = ð¥ for all ð¥, and a right unit
of ð¥ if ð¥ â ð = ð¥ for all ð¥. If ð is both a left unit and a right unit of an operator â, then it is called the unit of â. The
terminology is appropriate since only one value can be both a left and right unit. So, by proving that ðððð is a left unit , we
have proved that ðððð is the unit of +.
Richard Bird
16. Proof. The proof is by induction on ð. More precisely, we take for ð(ð) the assertion that ðððð + ð = ð. This equation is
referred to as the induction hypothesis.
Case (ðððð). We have to show ðððð + ðððð = ðððð, which is immediate from the first equation defining +.
Case (ðºððð ð). We have to show that ðððð + ðºððð ð = ðºððð ð, which we do by simplifying the left-hand expression:
ðððð + ðºððð ð
= { second equation for +, i.e. ð + ðºððð ð = ðºððð ð + ð }
ðºððð (ðððð + ð)
= { induction hypothesis}
ðºððð ð
â
This example shows the format we will use for inductive proofs, laying out each case separately and using a â to mark the end.
The very last step made use of the induction hypothesis, which is allowed by the way induction works.
âŠ
3.2.1 Full Induction
In the form given above, the induction principle for ðµðð suffices only to prove properties of the finite members of ðµðð. If we
want to show that a property ð also hold for every partial number, then we have to prove three things:
Case (â¥). That ð(â¥) holds.
Case (ðððð). That ð(ðððð) holds.
Case (ðºððð ð). That if ð(ð) holds, then ð(ðºððð ð) holds also.
We can omit the second case, but then we can conclude only that ð(ð) holds for every partial number. The reason the
principle is valid is that is that every partial number is either ⥠or of the form ðºððð ð for some partial number ð. Richard Bird
17. To illustrate, let us prove the somewhat counterintuitive result that ð + ð = ð for all numbers ð and all partial numbers ð.
Proof. The proof is by partial number induction on ð.
Case (â¥). The equation ð + ⥠= ⥠follows at once by case exhaustion in the definition of +. That is, ⥠does not match either of
the patterns ðððð or ðºððð ð.
Case (ðºððð ð). For the left-hand side, we reason
ð + ðºððð ð
= { second equation for +, i.e. ð + ðºððð ð = ðºððð ð + ð }
ðºððð (ð + ð)
= { induction hypothesis}
ðºððð ð
Since the right-hand side is also ðºððð ð, we are done.
3.2.2 Program synthesis
In the proofs above we defined some functions and then used induction to prove a certain property. We can also view
induction as a way to synthesise definitions of functions so that they satisfy the properties we want.
Let us illustrate with a simple example. Suppose we specify subtraction of natural numbers by the condition
ð + ð â ð = ð
for all ð and ð. The specification does not give a constructive definition of â , merely a property that it has to satisfy.
However, we can do an induction proof on ð of the equation above, but view the calculation as a way of generating a suitable
definition of â .
Richard Bird
18. Unlike previous proofs, we reason with the equation as a whole, since simplification of both sides independently is not
possible if we do not know what all the rules of simplification are.
Case (ðððð). We reason
ð + ðððð â ðððð = ð
â¡ { first equation for +, i.e. ð + ðððð = ð }
ð â ðððð = ð
Hence we can take ð â ðððð = ð to satisfy the case. The symbol â¡ is used to separate steps of the calculation since we are
calculating with mathematical assertions, not with values of a datatype.
Case (ðºððð ð). We reason
ð + ðºððð ð â ðºððð ð = ð
â¡ { second equation for +, i.e. ð + ðºððð ð = ðºððð ð + ð }
ðºððð ð + ð â ðºððð ð = ð
â¡ { hypothesis ð + ð â ð = ð}
ðºððð ð + ð â ðºððð ð = ð + ð â ð
Replacing ð + ð in the last equation by ð, we can take ðºððð ð â ðºððð ð = ð â ð to satisfy the case. Hence we have derived
ð â ðððð = ð
ðºððð ð â ðºððð ð = ð â ð
This is the program for â seen earlier.
Richard Bird
19. After that look at structural induction, it is finally time to
see how Richard Bird introduces the concept of folding.
20. 3.3 The fold function
Many of the recursive definitions seen so far have a common pattern, exemplified by the following definition of a function ð:
ð â· ðµðð â ðŽ
ð ðððð = ð
ð ðºððð ð = â ð ð
Here, ðŽ is some type, ð is an element of ðŽ, and â â· ðŽ â ðŽ . Observe that ð works by taking an element of ðµðð and replacing
ðððð by ð and ðºððð by â. For example, ð takes
ðºððð (ðºððð (ðºððð ðððð)) to â (â (â ð))
The two equations for ð can be captured in terms of a single function, ððððð, called the ðððð function for ðµðð. The definition is
ððððð â· ðŒ â ðŒ â ðŒ â ðµðð â ðŒ
ððððð â ð ðððð = ð
ððððð â ð ðºððð ð = â ððððð â ð ð
In particular, we have
ð + ð = ððððð ðºððð ð ð
ð Ã ð = ððððð + ð ðððð ð
ð â ð = ððððð à ð ðºððð ðððð ð
It follows also that the identity function ðð on ðµðð satisfies ðð = ððððð ðºððð ðððð. A suitable ðððð function can be defined for
every recursive type, and we will see other ðððð functions in the following chapters.
Richard Bird
21. + â· ðµðð â ðµðð â ðµðð
ð + ðððð = ð
ð + ðºððð ð = ðºððð ð + ð
(Ã) â· ðµðð â ðµðð â ðµðð
ð Ã ðððð = ðððð
ð à ðºððð ð = ð à ð + ð
(â) â· ðµðð â ðµðð â ðµðð
ð â ðððð = ðºððð ðððð
ð â ðºððð ð = ð â ð à ð
+ â· ðµðð â ðµðð â ðµðð
ð + ð = ððððð ðºððð ð ð
(Ã) â· ðµðð â ðµðð â ðµðð
ð Ã ð = ððððð + ð ðððð ð
(â) â· ðµðð â ðµðð â ðµðð
ð â ð = ððððð à ð ðºððð ðððð ð
ððððð â· ðŒ â ðŒ â ðŒ â ðµðð â ðŒ
ððððð â ð ðððð = ð
ððððð â ð ðºððð ð = â ððððð â ð ð
@philip_schwarz
Just to reinforce the ideas on the previous slide, here are the original definitions
of +, Ã and â, and next to them, the new definitions in terms of ððððð.
And the next slide is the same but in terms of Scala code.
22. val `(+)`: Nat => Nat => Nat =
m => {
case Zero => m
case Succ(n) => Succ(m + n)
}
val `(Ã)`: Nat => Nat => Nat =
m => {
case Zero => Zero
case Succ(n) => (m à n) + m
}
val `(â)`: Nat => Nat => Nat =
m => {
case Zero => Succ(Zero)
case Succ(n) => (m â n) Ã m
}
val `(Ã)`: Nat => Nat => Nat =
m => n => foldn((x:Nat) => x + m, Zero, n)
def foldn[A](h: A => A, c: A, n: Nat): A =
n match {
case Zero => c
case Succ(n) => h(foldn(h,c,n))
}
val `(â)`: Nat => Nat => Nat =
m => n => foldn((x:Nat) => x à m, Succ(Zero), n)
val `(+)`: Nat => Nat => Nat =
m => n => foldn(Succ,m,n)
23. In the examples above, each instance of ððððð also returned an element of ðµðð. In the following two examples, ððððð
returns an element of (ðµðð, ðµðð):
ðððð¡ â· ðµðð â ðµðð
ðððð¡ = ð ðð Å ððððð ð (ðððð, ðºððð ðððð)
where ð ð, ð = (ðºððð ð, ðºððð (ð) à ð)
ððð â· ðµðð â ðµðð
ððð = ðð ð¡ Å ððððð ð (ðððð, ðºððð ðððð)
where ð ð, ð = (ð, ð + ð)
The function ðððð¡ computes the factorial function and function ððð computes the Fibonacci function. Each program works by first
computing a more general result, namely an element of (ðµðð, ðµðð), and then extracts the required result. In fact,
ððððð ð ðððð, ðºððð ðððð ð = ð, ðððð¡ ð
ððððð ð ðððð, ðºððð ðððð ð = ððð ð, ððð ðºððð ð
These equations can be proved by induction. The program for ððð is more efficient than a direct recursive definition. The
recursive program requires an exponential number of + operations, while the program above requires only a linear number. We
will discuss efficiency in more detail in chapter 7, where the programming technique that led to the invention of the new program
for ððð will be studied in a more general setting.
There are two advantages of writing recursive definitions in terms of ððððð. Firstly, the definition is shorter; rather than having to
write down two equations, we have only to write down one. Secondly, it is possible to prove general properties of ððððð and use
them to prove properties of specific instantiations. In other words, rather than having to write down many induction proofs, we
have only to write down one.
Richard Bird
24. Now letâs have a very quick look at the
datatype for lists, and at induction over lists.
25. 4.1.1 Lists as a datatype
A list can be constructed from scratch by starting with the empty list and successively adding elements one by one. One can add
elements to the front of the list, or to the rear, or to somewhere in the middle. In the following datatype declaration, nonempty
lists are constructed by adding elements to the front of the list:
ðððð ð³ððð ðŒ = ðµðð | ðªððð ðŒ (ð³ððð ðŒ)
âŠThe constructor (short for âconstructâ â the name goes back to the programming language LISP) adds an element to the front of
the list. For example, the list 1,2,3 would be represented as the following element of ð³ððð ð°ðð:
ðªððð 1 (ðªððð 2 (ðªððð 3 ðµðð ))
In functional programming, lists are defined as elements of ð³ððð ðŒ. The syntax [ðŒ] is used instead of ð³ððð ðŒ, the constructor ðµðð is
written as [ ], and the constructor ðªððð is written as infix operator (â¶). Moreover, (â¶) associates to the right, so
1,2,3 = 1: 2: 3: [ ] = 1 ⶠ2 ⶠ3 ⶠ[ ]
In other words, the special syntax on the left can be regarded as an abbreviation for the syntax on the right, which is also special,
but only by virtue of the fact that the constructors are given nonstandard names.
Like functions over other datatypes, functions over lists can be defined by pattern matching.
Richard Bird
26. Before moving on to the topic of induction over lists, Richard Bird gives
an example of a function defined over lists using pattern matching, but
the function he chooses is the equality function, whereas we are going
to choose the sum function, just to keep things simpler.
ð ð¢ð â· [ð°ðð] â ð°ðð
ð ð¢ð [ ] = 0
ð ð¢ð ð¥: ð¥ð = ð¥ + (ð ð¢ð ð¥ð )
val sum : List[Int] => Int = {
case Nil => 0
case x :: xs => x + sum(xs)
}
assert( sum( 4 :: (5 :: (6 :: Nil)) ) == 15)
27. sealed trait Nat
case class Succ(n: Nat) extends Nat
case object Zero extends Nat
sealed trait List[+A]
case class Cons[+A](head: A, tail: List[A]) extends List[A]
case object Nil extends List[Nothing]
val `(+)`: Nat => Nat => Nat =
m => { case Zero => m
case Succ(n) => Succ(m + n) }
implicit class NatSyntax(m: Nat){
def +(n: Nat) = `(+)`(m)(n)
}
val sum: List[Nat] => Nat = {
case Nil => Zero
case Cons(x, xs) => x + sum(xs)
}
assert( sum( Cons(
Succ(Zero), // 1
Cons(
Succ(Succ(Zero)), // 2
Cons(
Succ(Succ(Succ(Zero))), // 3
Nil))) )
== Succ(Succ(Succ(Succ(Succ(Succ(Zero))))))) // 6
ð ð¢ð â· ð³ððð ðµðð â ðµðð
ð ð¢ð ðµðð = ðððð
ð ð¢ð ðªððð ð¥ ð¥ð = ð¥ + (ð ð¢ð ð¥ð )
+ â· ðµðð â ðµðð â ðµðð
ð + ðððð = ð
ð + ðºððð ð = ðºððð ð + ð
ðððð ðµðð = ðððð | ðºððð ðµðð
ðððð ðð¢ð¬ð α = ðð¢ð¥ | ððšð§ð¬ α (ðð¢ð¬ð α)
Same as on the previous slide, but this time
using Nat rather than Int, just for fun.
28. 4.1.2 Induction over Lists
Recall from section 3.2 that, for the datatype ðµðð of natural numbers, structural induction is based on three cases: every element
of ðµðð is either â¥, or ðððð, or else has the form ðºððð ð for some element ð of ðµðð. Similarly, structural induction on lists is also
based on on three cases: every list is either the undefined list â¥, the empty list [ ], or else has the form ð¥: ð¥ð for some ð¥ and list
ð¥ð .
To show by induction that a proposition ð(ð¥ð ) holds for all lists ð¥ð it suffices therefore to establish three cases:
Case (â¥). That ð(â¥) holds.
Case ([ ]). That ð([ ]) holds.
Case ð¥: ð¥ð . That if ð(ð¥ð ) holds, then ð(ð¥: ð¥ð ) also holds for every ð¥.
If we prove only the second two cases, then we can conclude only that ð(ð¥ð ) holds for every finite list; if we prove only the first
and third cases. Then we can conclude only that ð(ð¥ð ) holds for every partial list. If ð takes the form of an equation, as all of our
laws do, then proving the first and third cases is sufficient to show that ð(ð¥ð ) holds for every infinite list. Partial lists and infinite
lists are described in the following section. Examples of induction proofs are given throughout the remainder of the chapter.
Richard Bird
29. Richard Bird provides other examples of recursive functions
over lists. Letâs see some of them: list concatenation,
flattening of lists of lists, list reversal and length of a list.
When looking at the first one, i.e. concatenation, letâs also
see an example of proof by structural induction on lists.
@philip_schwarz
30. 4.2.1 Concatenation
Two lists can be concatenated to form one longer list. This function is denoted by the binary operator ⧺ (pronounced
âconcatenateâ). As two simple examples, we have
? 1,2,3 ⧺ 4,5
1,2,3,4,5
? 1,2 ⧺ ⧺ 1
1,2,1
The formal definition of ⧺ is
(⧺) â· [α] â [α] â [α]
⧺ ðŠð = ðŠð
ð¥: ð¥ð ⧺ ðŠð = ð¥ ⶠ(ð¥ð ⧺ ðŠð )
Concatenation takes two lists, both of the same type, and produces a third list, again of the same type. Hence the type assignment.
The definition of ⧺ is by pattern matching on the left-hand argument; the two patterns are disjoint and cover all cases, apart from
the undefined list â¥. It follows by case exhaustion that ⥠⧺ ðŠð = â¥.
However, it is not the case that ðŠð ⧺ ⥠= â¥. For example,
? 1,2,3 ⧺ ð¢ðððððððð
1,2,3{ðŒðð¡ðððð¢ðð¡ðð!}
The list 1,2,3 ⧺ ⥠is a partial list; In full form it is the list 1: 2: 3: â¥. The evaluator can compute the first three elements, but
thereafter it goes into a nonterminating computation, so we interrupt it.
The second equation for ⧺ is very succinct and requires some thought. Once one has come to grips with the definition of ⧺, one Richard Bird
31. has understood a good deal about how lists work in functional programming. Note that the number of steps required to compute
ð¥ð ⧺ ðŠð is proportional to the number of elements in ð¥ð .
1, 2 ⧺ 3, 4, 5
= { notation}
(1 ⶠ2 ⶠ⧺ (3 ⶠ(4 ⶠ5 ⶠ[ ] ))
= { second equation for ⧺, i.e. ð¥: ð¥ð ⧺ ðŠð = ð¥ ⶠ(ð¥ð ⧺ ðŠð ) }
1 ⶠ( 2 ⶠ⧺ (3 ⶠ(4 ⶠ5 ⶠ[ ] )))
= { second equation for ⧺ }
1 ⶠ(2 ⶠ( ⧺ (3 ⶠ(4 ⶠ5 ⶠ[ ] ))))
= { first equation for ⧺ i.e. , ⧺ ðŠð = ðŠð }
1 ⶠ(2 ⶠ(3 ⶠ(4 ⶠ5 ⶠ[ ] )))
= { notation}
1, 2, 3, 4, 5
Concatenation is an associative operation with with unit :
ð¥ð ⧺ ðŠð ⧺ ð§ð = ð¥ð ⧺ (ðŠð ⧺ ð§ð )
ð¥ð ⧺ = ⧺ ð¥ð = ð¥ð
Let us now prove by induction that ⧺ is associative.
Proof. The proof is by induction on ð¥ð .
Case (â¥). For the left-hand side, we reason
⥠⧺ (ðŠð ⧺ ð§ð )
= { case exhaustion}
⥠⧺ ð§ð
= { case exhaustion}
â¥
Richard Bird
32. The right-hand side simplifies to ⥠as well, establishing the case.
Case ([ ]). For the left hand side, we reason
[ ] ⧺ (ðŠð ⧺ ð§ð )
= { first equation for ⧺ i.e. , ⧺ ðŠð = ðŠð }
(ðŠð ⧺ ð§ð )
The right-hand side simplifies to(ðŠð ⧺ ð§ð ) as well, establishing the case.
Case ð¥ ⶠð¥ð . For the left hand side, we reason
((x ⶠð¥ð ) ⧺ ðŠð ) ⧺ ð§ð
= { second equation for ⧺, i.e. ð¥: ð¥ð ⧺ ðŠð = ð¥ ⶠ(ð¥ð ⧺ ðŠð ) }
( ð¥ ⶠð¥ð ⧺ ðŠð ) ⧺ ð§ð
= { second equation for ⧺ }
ð¥ ⶠ( ð¥ð ⧺ ðŠð ⧺ ð§ð )
= { induction hypothesis }
ð¥ ⶠ(ð¥ð ⧺ ðŠð ⧺ ð§ð )
For the right-hand side we reason
(x ⶠð¥ð ) ⧺ (ðŠð ⧺ ð§ð )
= { second equation for ⧺, i.e. ð¥ ⶠð¥ð ⧺ ðŠð = ð¥ ⶠ(ð¥ð ⧺ ðŠð ) }
ð¥ ⶠ(ð¥ð ⧺ (ðŠð ⧺ ð§ð ))
The two sides are equal, establishing the case.
âŠNote that associativity is proved for all lists, finite, partial or infinite. Hence we can assert that ⧺ is associative without
qualificationâŠ. Richard Bird
33. 4.2.2 Concat
Concatenation performs much the same function for lists as the union operator ⪠does for sets. A companion function is concat,
which concatenates a list of lists into one long list. This function, which roughly corresponds to the big-union operator â for sets
of sets, is defined by
concat â· [ α ] â [α]
concat =
concat ð¥ð : ð¥ð ð = ð¥ð ⧺ ðððððð¡ ð¥ð ð
For example,
?[ 1, 2 ⧺ ⧺ 3, 2,1 ]
1,2,3, 2,1
4.2.3 Reverse
Another basic function on lists is reverse, the function that reverses the order of elements in a finite list. For example:
? reverse âMadam, Iâm Adam.â
â.MadA mâI ,madaMâ
The definition is
reverse ⷠα â [α]
reverse =
reverse ð¥ ⶠð¥ð = ððð£ððð ð ð¥ð ⧺ [ð¥]
In words, to reverse a list ð¥ ⶠð¥ð , one reverses ð¥ð , and then adds ð¥ to the end. As a program, the above definition is not very Richard Bird
34. efficient: on a list of length ð, it will need a number of reduction steps proportional to ð2 to deliver the reversed list. The first
element will be appended to the end of a list of length ð â 1 , which will take about ð â 1 steps, the second element will be
appended to a list of length ð â 2 , taking ð â 2 steps, and so on. The total time is therefore about
ð â 1 + ð â 2 + ⯠1 = ð(ð â 1)/2 steps
A more precise analysis is given in chapter 7, and a more efficient program for reverse is given in section 4.5.
4.2.2 Length
The length of a list is the number of elements it contains:
ððððð¡â â· [α] â ð°ðð
ððððð¡â [ ] = 0
ððððð¡â ð¥: ð¥ð = 1 + ððððð¡â ð¥ð
The nature of the list element is irrelevant when computing the length of a list, whence the type assignment. For example,
? ððððð¡â [ð¢ðððððððð, ð¢ðððððððð]
2
However, not every list has a well-defined length. In particular, the partial lists â¥, ð¥ ⶠâ¥, ð¥ ⶠðŠ ⶠâ¥, and so on, have an undefined
length. Only finite lists have well-defined lengths. The list â¥, ⥠is a finite list, not a partial list, because it is the list ⥠ⶠ⥠ⶠ[ ],
which ends in [ ], not â¥. The computer cannot produce the elements, but it can produce the length of the list.
âŠ
Richard Bird
35. 4.3 Map and filter
Two useful functions on lists are map and £ilter. The function map applies a function to each element of a list. For example
? ðap square [9, 3] ? ðap (<3) [1, 2, 3] ? ðap nextLetter âHALâ
[81, 9] [ððð¢ð, ððð¢ð, ð¹ððð ð] âIBMâ
The definition is
map â· (α â ðœ) â [α] â [ðœ]
map f =
map f ð¥ ⶠð¥ð = ð ð¥ ⶠmap ð ð¥ð
âŠ
4.3 filter
The second function, filter, takes a Boolean function p and a list xs and returns that sublist of xs whose elements satisfy p. For
example,
? £ilter even [1,2,4,5,32] ? (sum Šmap square Š£ilter even) [1. . 10]
[2,4,32] 220
The last example asks for the sum of the squares of the even integers in the range 1..10.
The definition of filter is
£ilter â· (α â ðµððð) â [α] â [α]
£ilter p =
£ilter p ð¥ ⶠð¥ð = ð¢ð ð ð¥ ðð¡ðð§ ð¥ ⶠ£ilter p ð¥ð ðð¥ð¬ð £ilter p ð¥ð
⊠Richard Bird
37. 4.5 The fold functions
We have seen in the case of the datatype ðµðð that many recursive definitions can be expressed very succinctly using a
suitable ðððð operator. Exactly the same is true of lists. Consider the following definition of a function â :
â [ ] = ð
â ð¥: ð¥ð = ð¥ â â ð¥ð
The function â works by taking a list, replacing [ ] by ð and ⶠby â, and evaluating the result. For example, â converts the list
ð¥1 ⶠ(ð¥2 ⶠð¥3 ⶠð¥4 ⶠ)
to the value
ð¥1 â (ð¥2 â (ð¥3 â ð¥4 â ð ))
Since ⶠassociates to the right, there is no need to put in parentheses in the first expression. However, we do need to put in
parentheses in the second expression because we do not assume that â associates to the right.
The pattern of definition given by â is captured in a function ððððð (prounced âfold rightâ) defined as follows:
ððððð â· ðŒ â ðœ â ðœ â ðœ â ðŒ â ðœ
ððððð ð ð = ð
ððððð ð ð ð¥: ð¥ð = ð ð¥ ððððð ð ð ð¥ð
We can now write h = ððððð â ð. The first argument of ððððð is a binary operator that takes an ðŒ-value on its left and an a ðœâ
value on its right, and delivers a ðœâvalue. The second argument of ððððð is a ðœ-value. The third argument is of type ðŒ , and the
result is of type ðœ. In many cases, ðŒ and ðœ will be instantiated to the same type, for instance when â denotes an associative
operation. Richard Bird
38. In the next slide we look at how some of the recursively
defined functions on lists that we have recently seen can be
redefined in terms of ððððð.
To aid comprehension, I have added the original function
definitions next to the new definitions in terms of ððððð. For
reference, I also added the definition of ððððð.
40. On the next slide, the same code, translated into Scala
@philip_schwarz
41. def foldr[A,B](f: A => B => B)(e: B)(xs: List[A]): B =
xs match {
case Nil => e
case x::xs => f(x)(foldr(f)(e)(xs))
}
def concatenate[A]: List[A] => List[A] => List[A] =
xs => ys => xs match {
case Nil => ys
case x :: xs => x :: concatenate(xs)(ys)
}
def concat[A]: List[List[A]] => List[A] =
foldr(concatenate[A])(Nil)
def reverse[A]: List[A] => List[A] = {
def snoc[A]: A => List[A] => List[A] =
x => xs => concatenate(xs)(List(x))
foldr(snoc[A])(Nil)
}
def length[A]: List[A] => Int = {
def oneplus[A]: A => Int => Int = x => n => 1 + n
foldr(oneplus)(0)
}
val sum: List[Int] => Int = {
val plus: Int => Int => Int = a => b => a + b
foldr(plus)(0)
}
def map[A,B]: (A => B) => List[A] => List[B] = {
def cons: B => List[B] => List[B] = x => xs => x :: xs
f => foldr(cons compose f)(Nil)
}
ððððð â· ðŒ â ðœ â ðœ â ðœ â ðŒ â ðœ
ððððð ð ð = ð
ððððð ð ð ð¥: ð¥ð = ð ð¥ ððððð ð ð ð¥ð
(⧺) â· [α] â [α] â [α]
⧺ ðŠð = ðŠð
ð¥: ð¥ð ⧺ ðŠð = ð¥ ⶠ(ð¥ð ⧺ ðŠð )
concat â· [ α ] â [α]
concat = ððððð (⧺) [ ]
reverse ⷠα â [α]
reverse = ððððð ð ððð
ð€âððð ð ððð ð¥ ð¥ð = ð¥ð ⧺ [ð¥]
ððððð¡â â· [α] â ð°ðð
ððððð¡â = ððððð ðððððð¢ð 0
ð€âððð ðððððð¢ð ð¥ ð = 1 + ð
ð ð¢ð â· [ð°ðð] â ð°ðð
ð ð¢ð = ððððð + 0
map â· (α â ðœ) â [α] â [ðœ]
map ð = ððððð ðððð Q ð
ð€âððð ðððð ð¥ ð¥ð = ð¥ ⶠð¥ð
42. assert( concatenate(List(1,2,3))(List(4,5)) == List(1,2,3,4,5) )
assert( concat(List(List(1,2), List(3), List(4,5))) == List(1,2,3,4,5) )
assert( reverse(List(1,2,3,4,5)) == List(5,4,3,2,1) )
assert( length(List(0,1,2,3,4,5)) == 6 )
assert( sum(List(2,3,4)) == 9 )
val mult: Int => Int => Int = a => b => a * b
assert( map(mult(10))(List(1,2,3)) == List(10,20,30))
Here a some sample tests for the
Scala functions on the previous slide.
43. It turns out that if it is possible to define a function on lists both using a recursive definition and
using a definition in terms of ððððð, then there is a technique that can be used to go from the
recursive definition to the definition using ððððð.
I came across the technique in the following paper by the author of Programming in Haskell:
The tutorial (which I shall be referring to as TUEF), shows how to apply the technique to the
sum function and the map function, which is the subject of the next five slides. Note: in the
paper, the ððððð function is referred to as ðððð.
@philip_schwarz
44. 3 The universal property of fold
As with the fold operator itself, the universal property of ðððð also has its origins in recursion theory. The first systematic use of the
universal property in functional programming was by Malcolm (1990a), in his generalisation of Bird and Meertenâs theory of lists (Bird,
1989; Meertens, 1983) to arbitrary regular datatypes. For finite lists, the universal property of ðððð can be stated as the following
equivalence between two definitions for a function ð that processes lists:
ð = ð£ ⺠ð = ðððð ð ð£
ð ð¥ ⶠð¥ð = ð ð¥ ð ð¥ð
In the right-to-left direction, substituting ð = ðððð ð ð£ into the two equations for ð gives the recursive definition for ðððð. Conversely,
in the left-to-right direction the two equations for g are precisely the assumptions required to show that ð = ðððð ð ð£ using a simple
proof by induction on finite lists (Bird, 1998). Taken as a whole, the universal property states that for finite lists the function ðððð ð ð£ is
not just a solution to its defining equations, but in fact the unique solutionâŠ. The universal property of ðððð can be generalised to
handle partial and infinite lists (Bird, 1998), but for simplicity we only consider finite lists in this article.
Graham Hutton
@haskellhutt
45. 3.3 Universality as a definition principle
As well as being used as a proof principle, the universal property of ðððð can also be used as a definition principle that guides the
transformation of recursive functions into definitions using ðððð. As a simple first example, consider the recursively defined function
ð ð¢ð that calculates the sum of a list of numbers:
ð ð¢ð â· ðŒðð¡ â ðŒðð¡
ð ð¢ð = 0 ð
ð ð¢ð ð¥ ⶠð¥ð = ð¥ + ð ð¢ð ð¥ð
Suppose now that we want to redefine ð ð¢ð using ðððð. That is, we want to solve the equation ð ð¢ð = ðððð ð ð£ for a function f and a
value ð£. We begin by observing that the equation matches the right-hand side of the universal property, from which we conclude that
the equation is equivalent to the following two equations:
ð ð¢ð = ð£
ð ð¢ð ð¥ ⶠð¥ð = ð ð¥ (ð ð¢ð ð¥ð )
From the first equation and the definition of ð ð¢ð, it is immediate that ð£ = 0.
ð = ð£ ⺠ð = ðððð ð ð£
ð ð¥ ⶠð¥ð = ð ð¥ ð ð¥ð
Graham Hutton
@haskellhutt
46. From the second equation, we calculate a definition for f as follows:
ð ð¢ð ð¥ ⶠð¥ð = ð ð¥ (ð ð¢ð ð¥ð )
â { Definition of ð ð¢ð }
ð¥ + ð ð¢ð ð¥ð = ð ð¥ (ð ð¢ð ð¥ð )
â { â Generalising (ð ð¢ð ð¥ð ) to ðŠ }
ð¥ + ðŠ = ð ð¥ ðŠ
â { Functions }
ð = (+)
That is, using the universal property we have calculated that:
ð ð¢ð = ðððð + 0
Note that the key step (â ) above in calculating a definition for ð is the generalisation of the expression ð ð¢ð ð¥ð to a fresh variable ðŠ. In fact,
such a generalisation step is not specific to the ð ð¢ð function, but will be a key step in the transformation of any recursive function into a
definition using fold in this manner.
ð ð¢ð â· ðŒðð¡ â ðŒðð¡
ð ð¢ð = 0 ð
ð ð¢ð ð¥ ⶠð¥ð = ð¥ + ð ð¢ð ð¥ð
ð ð¢ð = ð£
ð ð¢ð ð¥ ⶠð¥ð = ð ð¥ (ð ð¢ð ð¥ð )
Graham Hutton
@haskellhutt
47. Of course, the ð ð¢ð example above is rather artificial, because the definition of ð ð¢ð using ðððð is immediate. However, there are many
examples of functions whose definition using ðððð is not so immediate. For example, consider the recursively defined function ððð ð that
applies a function ð to each element of a list:
ððð â· ðŒ â ðœ â ðŒ â ðœ
ððð ð =
ððð ð ð¥ ⶠð¥ð = ð ð¥ ⶠððð ð ð¥ð
To redefine ððð ð using ðððð we must solve the equation ððð ð = ðððð ð£ ð for a function ð and a value ð£. By appealing to the
universal property, we conclude that this equation is equivalent to the following two equations:
ððð ð = ð£
ððð ð ð¥ ⶠð¥ð = ð ð¥ (ððð ð ð¥ð )
From the first equation and the definition of ððð it is immediate that ð£ = [ ].
Graham Hutton
@haskellhutt
ð = ð£ ⺠ð = ðððð ð ð£
ð ð¥ ⶠð¥ð = ð ð¥ ð ð¥ð
substitute ððð ð for ð and ð for ð
48. From the second equation, we calculate a definition for ð as follows:
ððð ð ð¥ ⶠð¥ð = ð ð¥ (ððð ð ð¥ð )
â { Definition of ððð }
ð ð¥ ⶠððð ð ð¥ð = ð ð¥ (ððð ð ð¥ð )
âž { Generalising (ððð ð ð¥ð ) to ðŠð }
ð ð¥ ⶠðŠð = ð ð¥ ðŠð
â { Functions }
ð = ðð¥ ðŠð â ð ð¥ ⶠðŠð
That is, using the universal property we have calculated that
ððð ð = ðððð ðð¥ ðŠð â ð ð¥ ⶠðŠð [ ]
In general, any function on lists that can be expressed using the ðððð operator can be transformed into such a definition using the
universal property of ðððð.
Graham Hutton
@haskellhutt
ððð â· ðŒ â ðœ â ðŒ â ðœ
ððð ð = ð£
ððð ð ð¥ ⶠð¥ð = ð ð¥ (ððð ð ð¥ð )
49. There are several other interesting things in TUEF that weâll be looking at.
I like its description of ððððð (see right), because it reiterates a key point (see left) made by Richard Bird about recursive functions on lists.
Consider the following definition of a function â :
â [ ] = ð
â ð¥: ð¥ð = ð¥ â â ð¥ð
The function â works by taking a list, replacing [ ] by ð and ⶠby â, and evaluating
the result. For example, â converts the list
ð¥1 ⶠ(ð¥2 ⶠð¥3 ⶠð¥4 ⶠ)
to the value
ð¥1 â (ð¥2 â (ð¥3 â ð¥4 â ð ))
Since ⶠassociates to the right, there is no need to put in parentheses in the first
expression. However, we do need to put in parentheses in the second expression
because we do not assume that â associates to the right.
The pattern of definition given by â is captured in a function ððððð (prounced âfold
rightâ) defined as follows:
ððððð â· ðŒ â ðœ â ðœ â ðœ â ðŒ â ðœ
ððððð ð ð = ð
ððððð ð ð ð¥: ð¥ð = ð ð¥ ððððð ð ð ð¥ð
2 The fold operator
The fold operator has its origins in recursion theory (Kleene, 1952), while the use of fold as
a central concept in a programming language dates back to the reduction operator of APL
(Iverson, 1962), and later to the insertion operator of FP (Backus, 1978). In Haskell, the fold
operator for lists can be defined as follows:
ðððð :: ðŒ â ðœ â ðœ â ðœ â ðŒ â ðœ
ðððð ð ð£ = ð£
ðððð ð ð£ ð¥ ⶠð¥ð = ð ð¥ ðððð ð ð£ ð¥ð
That is, given a function f of type ðŒ â ðœ â ðœ and a value ð£ of type ðœ, the function
ðððð ð ð£ processes a list of type ðŒ to give a value of type ðœ by replacing the nil
constructor at the end of the list by the value ð£, and each cons constructor ⶠwithin
the list by the function ð. In this manner, the ðððð operator encapsulates a simple pattern
of recursion for processing lists, in which the two constructors for lists are simply
replaced by other values and functions.
50. (⧺) â· [α] â [α] â [α]
⧺ ðŠð = ðŠð
ð¥: ð¥ð ⧺ ðŠð = ð¥ ⶠ(ð¥ð ⧺ ðŠð )
Concatenation takes two lists, both of
the same type, and produces a third
list, again of the same type.
Remember the list concatenation function we saw earlier?
In TUEF we find a definition of concatenation in terms of ððððð (which it calls ðððð)
(⧺) â· [α] â [α] â [α]
⧺ ðŠð = ðððð ⶠðŠð
assert( concatenate(List(1,2,3))(List(4,5)) == List(1,2,3,4,5) )
def concatenate[A]: List[A] => List[A] => List[A] =
xs => ys => xs match {
case Nil => ys
case x :: xs => x :: concatenate(xs)(ys)
}
def concatenate[A]: List[A] => List[A] => List[A] = {
def cons: A => List[A] => List[A] =
x => xs => x :: xs
xs => ys => foldr(cons)(ys)(xs)
}
51. Remember the £ilter function we saw earlier?
In TUEF we find a definition of £ilter in terms of ððððð (which as we saw, it calls ðððð)
£ilter â· (α â ðµððð) â [α] â [α]
£ilter p =
£ilter p ð¥ ⶠð¥ð = ð¢ð ð ð¥ ðð¡ðð§ ð¥ ⶠ£ilter p ð¥ð ðð¥ð¬ð £ilter p ð¥ð
£ilter â· (α â ðµððð) â [α] â [α]
£ilter p = ðððð (ðð¥ ð¥ð â ð¢ð ð ð¥ ðð¡ðð§ ð¥ ⶠð¥ð ðð¥ð¬ð ð¥ð ) [ ]
def filter[A]: (A => Boolean) => List[A] => List[A] = p => {
case Nil => Nil
case x :: xs => if (p(x)) x :: filter(p)(xs) else filter(p)(xs)
}
def filter[A]: (A => Boolean) => List[A] => List[A] = p =>
foldr((x:A) => (xs:List[A]) => if (p(x)) (x::xs) else xs)(Nil)
val gt: Int => Int => Boolean = x => y => y > x
assert(filter(gt(5))(List(10,2,8,5,3,6)) == List(10,8,6))
52. Not every function on lists can be defined as an instance of ððððð. For example, zip cannot be so defined. Even for those that
can, an alternative definition may be more efficient. To illustrate, suppose we want a function decimal that takes a list of digits
and returns the corresponding decimal number; thus
ððððððð [ð¥0, ð¥1, ⊠, ð¥n] = â!"#
$
ð¥ ð10($&!)
It is assumed that the most significant digit comes first in the list. One way to compute decimal efficiently is by a process of
multiplying each digit by ten and adding in the following digit. For example
ððððððð ð¥0, ð¥1, ð¥2 = 10 à 10 à 10 à 0 + ð¥0 + ð¥1 + ð¥2
This decomposition of a sum of powers is known as Hornerâs rule.
Suppose we define â by ð â ð¥ = 10 à ð + ð¥. Then we can rephrase the above equation as
ððððððð ð¥0, ð¥1, ð¥2 = (0 â ð¥0) â ð¥1 â ð¥2
This is almost like an instance of ððððð, except that the grouping is the other way round, and the starting value appears on the
left, not on the right. In fact the computation is dual: instead of processing from right to left, the computation processes from
left to right.
This example motivates the introduction of a second fold operator called ððððð (pronounced âfold leftâ). Informally:
ððððð â ð ð¥0, ð¥1, ⊠, ð¥ð â 1 = ⊠((ð â ð¥0) â ð¥1) ⊠â ð¥ ð â 1
The parentheses group from the left, which is the reason for the name. The full definition of ððððð is
ððððð â· ðœ â ðŒ â ðœ â ðœ â ðŒ â ðœ
ððððð ð ð = ð
ððððð ð ð ð¥: ð¥ð = ððððð ð ð ð ð¥ ð¥ð Richard Bird
53. For example
ððððð â ð ð¥0, ð¥1, ð¥2
= ððððð â ð â ð¥0 ð¥1, ð¥2
= ððððð â ð â ð¥0 â ð¥1 ð¥2
= ððððð â (( ð â ð¥0 â ð¥1) â ð¥2) [ ]
= ((ð â ð¥0) â ð¥1) â ð¥2
If â is associative with unit ð, then ððððð â ð and ððððð â ð define the same function on finite lists, as we will see in
the following section.
As another example of the use of ððððð, consider the following definition:
reverseⲠⷠα â [α]
reverseâ² = ððððð ðððð
ð€âððð ðððð ð¥ð ð¥ = ð¥ : ð¥ð
Note the order of the arguments to cons; we have ðððð = ðððð (â¶), where the standard function ðððð is defined by ððððð ð¥ ðŠ =
ð ðŠ ð¥. The function reverseâ² , reverses a finite list. For example:
reverseâ² ð¥0, ð¥1, ð¥2
= ðððð (ðððð (ðððð [ ] ð¥0) ð¥1) ð¥2
= ðððð (ðððð ð¥1 ð¥0) ð¥2
= ðððð ð¥1, ð¥0 ð¥2
= ð¥2, ð¥1, ð¥0
One can prove that reverseâ² = reverse by induction, or as an instance of a more general result in the following section. Of
greater importance than the mere fact that reverse can be defined in a different way, is that reverseâ² gives a much more
efficient program: reverseâ² takes time proportional to n on a list of length ð, while reverse takes time proportional to ð2.
reverse ⷠα â [α]
reverse = ððððð ð ððð
ð€âððð ð ððð ð¥ ð¥ð = ð¥ð ⧺ [ð¥]
Richard Bird
54. def reverse'[A]: List[A] => List[A] = {
def cons: List[A] => A => List[A] =
xs => x => x :: xs
foldl(cons)(Nil)
}
assert( reverse'(List(1,2,3,4,5)) == List(5,4,3,2,1) )
def reverse[A]: List[A] => List[A] = {
def snoc[A]: A => List[A] => List[A] =
x => xs => concatenate(xs)(List(x))
foldr(snoc[A])(Nil)
}
def concatenate[A]: List[A] => List[A] => List[A] = {
def cons: A => List[A] => List[A] =
x => xs => x :: xs
xs => ys => foldr(cons)(ys)(xs)
}
assert( reverse(List(1,2,3,4,5)) == List(5,4,3,2,1) )
(⧺) â· [α] â [α] â [α]
⧺ ðŠð = ðððð ⶠðŠð
reverseⲠⷠα â [α]
reverseâ² = ððððð ðððð
ð€âððð ðððð ð¥ð ð¥ = ð¥ : ð¥ð
reverse ⷠα â [α]
reverse = ððððð ð ððð
ð€âððð ð ððð ð¥ ð¥ð = ð¥ð ⧺ [ð¥]
Here we can see the Scala version of
reverseâ, and how it compares with reverse
@philip_schwarz
55. Thatâs it for part 1. I hope you enjoyed that.
There is still a lot to cover of course, so Iâll see you in part 2.