For functions that can be defined both as an instance of a right fold and as an instance of a left fold, one may be more efficient than the other.
Let's look at the example of a function 'decimal' that converts a list of digits into the corresponding decimal number.
Erratum: it has been pointed out that it is possible to define the zip function using a right fold (see slide 5).
2. We want to write function π ππππππ, which given the digits of an integer number
[π0, π1, β¦ , ππ]
computes the integer value of the number
-
&'(
)
ππ β 10)*&
Thanks to the universal property of fold, if we are able to define π ππππππ so that its equations match those on the left hand side of the
following equivalence, then we are also able to implement π ππππππ using a right fold
i.e. given πππππ
πππππ :: πΌ β π½ β π½ β π½ β πΌ β π½
πππππ π π£ = π£
πππππ π π£ π₯ βΆ π₯π = π π₯ πππππ π π£ π₯π
we can reimplement π ππππππ like this:
π ππππππ = πππππ π π£
The universal property of ππππ
π = π βΊ π = ππππ π π π :: πΌ β π½
π π₯ βΆ π₯π = π π₯ π π₯π π :: π½
π :: πΌ β π½ β π½
scala> decimal(List(1,2,3,4))
val res0: Int = 1234
haskell> decimal [1,2,3,4]
1234
π0 β 103 + π1 β 102 + π2 β 101 + π3 β 100 = 1 β 1000 + 2 β 100 + 3 β 10 + 4 β 1 = 1234
3. Notice that π has two parameters: the head of the list, and the result of recursively calling π with the tail of the list
π π₯ βΆ π₯π = π π₯ π π₯π
In order to define our π ππππππ function however, the two parameters of π are not sufficient. When π ππππππ is passed [ππ, β¦ , ππ], π is
passed digit ππ, so π needs π and π in order to compute 10)*&
, but π β π is the number of elements in [ππ, β¦ , ππ] minus one, so by nesting
the definition of π inside that of π ππππππ, we can avoid explicitly adding a third parameter to π :
We nested π inside π ππππππ, so that the equations of π ππππππ match (almost) those of π. They donβt match perfectly, in that the π nested
inside π ππππππ depends on π ππππππβs list parameter, whereas the π nested inside π does not depend on πβs list parameter. Are we still able
to redefine π ππππππ using πππππ? If the match had been perfect, we would be able to define π ππππππ = πππππ π 0 (with π = 0), but
because π needs to know the value of π β π, we canβt just pass π to πππππ, and use 0 as the initial accumulator. Instead, we need to use (0, 0)
as the accumulator (the second 0 being the initial value of π β π, when π = π), and pass to πππππ a helper function β that manages π β π
and that wraps π, so that the latter has access to π β π.
def h(d: Int, acc: (Int,Int)): (Int,Int) = acc match { case (ds, e) =>
def f(d: Int, ds: Int): Int =
d * Math.pow(10, e).toInt + ds
(f(d, ds), e + 1)
}
def decimal(ds: List[Int]): Int =
ds.foldRight((0,0))(h).head
h :: Int -> (Int,Int) -> (Int,Int)
h d (ds, e) = (f d ds, e + 1) where
f :: Int -> Int -> Int
f d ds = d * (10 ^ e) + ds
decimal :: [Int] -> Int
decimal ds = fst (foldr h (0,0) ds)
def decimal(digits: List[Int]): Int =
val e = digits.length-1
def f(d: Int, ds: Int): Int =
d * Math.pow(10, e).toInt + ds
digits match
case Nil => 0
case d +: ds => f(d, decimal(ds))
decimal :: [Int] -> Int
decimal [] = 0
decimal (d:ds) = f d (decimal ds) where
e = length ds
f :: Int -> Int -> Int
f d ds = d * (10 ^ e) + ds
The unnecessary complexity
of the π ππππππ functions
on this slide is purely due to
them being defined in terms
of π . See next slide for
simpler refactored versions
in which π is inlined.
4. def f(d: Int, acc: (Int,Int)): (Int,Int) = acc match
case (ds, e) => (d * Math.pow(10, e).toInt + ds, e + 1)
def decimal(ds: List[Int]): Int =
ds.foldRight((0,0))(f).head
f :: Int -> (Int,Int) -> (Int,Int)
f d (ds, e) = (d * (10 ^ e) + ds, e + 1)
decimal :: [Int] -> Int
decimal ds = fst (foldr f (0,0) ds)
def decimal(digits: List[Int]): Int = digits match
case Nil => 0
case d +: ds => d * Math.pow(10, ds.length).toInt + decimal(ds)
decimal :: [Int] -> Int
decimal [] = 0
decimal (d:ds) = d*(10^(length ds))+(decimal ds)
Same π ππππππ functions as on the previous slide, but refactored as follows:
1. inlined π in all four functions
2. inlined e in the first two functions
3. renamed π to π in the last two functions
5. 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
The definition of π ππππππ using a right fold is inefficient because it computes β!"#
$
ππ β 10$&!
by computing 10$&!
for each π.
6. If we look back at our initial recursive definition of π ππππππ, we see that it splits its list parameter into a head and a tail.
If we get π ππππππ to split the list into init and last, we can make it more efficient by using Hornerβs rule:
We can then improve on that by going back to splitting the list into a head and a tail, and making π ππππππ tail recursive:
And finally, we can improve on that by defining π ππππππ using a left fold:
(β) :: Int -> Int -> Int
n β d = 10 * n + d
decimal :: [Int] -> Int
decimal [] = 0
decimal (d:ds) = d*(10^(length ds)) + (decimal ds)
def decimal(digits: List[Int]): Int = digits match
case Nil => 0
case d +: ds => d * Math.pow(10, ds.length).toInt + decimal(ds)
extension (n: Int)
def β(d Int): Int = 10 * n + d
decimal :: [Int] -> Int -> Int
decimal [] acc = acc
decimal (d:ds) acc = decimal ds (acc βd)
def decimal(ds: List[Int], acc: Int=0): Int = digits match
case Nil => acc
case d +: ds => decimal(ds, acc β d)
decimal :: [Int] -> Int
decimal = foldl (β) 0
decimal :: [Int] -> Int
decimal [] = 0
decimal ds = (decimal (init ds)) β (last ds)
def decimal(digits: List[Int]): Int = digits match
case Nil => 0
case ds :+ d => decimal(ds) β d
def decimal(ds: List[Int]): Int =
ds.foldLeft(0)(_β_)
7. Recap
In the case of the π ππππππ function, defining it using a left fold is simple and mathematically more efficient
whereas defining it using a right fold is more complex and mathematically less efficient
def decimal(ds: List[Int]): Int =
ds.foldRight((0,0))(f).head
def f(d: Int, acc: (Int,Int)): (Int,Int) = acc match
case (ds, e) => (d * Math.pow(10, e).toInt + ds, e + 1)
decimal :: [Int] -> Int
decimal ds = fst (foldr f (0,0) ds)
f :: Int -> (Int,Int) -> (Int,Int)
f d (ds, e) = (d * (10 ^ e) + ds, e + 1)
decimal :: [Int] -> Int
decimal = foldl (β) 0
(β) :: Int -> Int -> Int
n β d = 10 * n + d
def decimal(ds: List[Int]): Int =
ds.foldLeft(0)(_β_)
extension (n: Int)
def β(d Int): Int = 10 * n + d
π ππππππ 1,2,3,4 = π0 β 103 + (π1 β 102 + (π2 β 101 + (π3 β 100 + 0))) = 1 β 1000 + (2 β 100 + (3 β 10 + (4 β 1 + 0))) = 1234
π ππππππ 1,2,3,4 = 10 β 10 β 10 β 10 β 0 + π0 + π1 + π2 + π3 = 10 β (10 β 10 β 10 β 0 + 1 + 2 + 3) + 4 = 1234