Share
Embed
Transcript
21 slides
-
CHEAT-SHEET Folding #7 โถ / \ ๐๐ โถ / \
๐๐ โถ / \ ๐๐ โถ / \ ๐๐ ๐ / \ ๐๐ ๐ / \ ๐๐ ๐ / \ ๐๐ ๐ / \ ๐๐ ๐ @philip_schwarz slides by https://fpilluminated.com/ -
The Three Duality Theorems of Fold (for all finite lists
) ๐๐๐๐๐ โ ๐ ๐ฅ๐ = ๐๐๐๐๐ โ ๐ ๐ฅ๐ ๐๐๐๐๐ โ ๐ ๐ฅ๐ = ๐๐๐๐๐ โ ๐ ๐ฅ๐ ๐๐๐๐๐ ๐ ๐ ๐ฅ๐ = ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ (๐๐๐ฃ๐๐๐ ๐ ๐ฅ๐ ) where โ and ๐ are such that for all ๐ฅ, ๐ฆ, and ๐ง we have ๐ฅ โ ๐ฆ โ ๐ง = ๐ฅ โ ๐ฆ โ ๐ง ๐ โ ๐ฅ = ๐ฅ and ๐ฅ โ ๐ = ๐ฅ In other words, โ is associative with unit ๐. where โ, โ, and ๐ are such that for all ๐ฅ, ๐ฆ, and ๐ง we have ๐ฅ โ ๐ฆ โ ๐ง = ๐ฅ โ ๐ฆ โ ๐ง ๐ฅ โ ๐ = ๐ โ ๐ฅ In other words, โ and โ associate with each other, and ๐ on the right of โ is equivalent to ๐ on the left of โ. where ๐๐๐๐ ๐ ๐ฅ ๐ฆ = ๐ ๐ฆ ๐ฅ 1 2 3 ๐๐๐๐๐ โท ๐ฝ โ ๐ผ โ ๐ฝ โ ๐ฝ โ ๐ผ โ ๐ฝ ๐๐๐๐๐ ๐ ๐ = ๐ ๐๐๐๐๐ ๐ ๐ ๐ฅ: ๐ฅ๐ = ๐๐๐๐๐ ๐ ๐ ๐ ๐ฅ ๐ฅ๐ ๐๐๐๐๐ โท ๐ผ โ ๐ฝ โ ๐ฝ โ ๐ฝ โ ๐ผ โ ๐ฝ ๐๐๐๐๐ ๐ ๐ = ๐ ๐๐๐๐๐ ๐ ๐ ๐ฅ: ๐ฅ๐ = ๐ ๐ฅ ๐๐๐๐๐ ๐ ๐ ๐ฅ๐ โ โก Theorem is a special case of with โ = โ 1 2 โ Theorem is a generalisation of 2 1 โก โ For example, + and ร are associative operators with respective units 0 and 1. โ โฉ โฉ Except lists sufficiently large to cause a right fold to encounter a stack overflow -
:{ foldRight :: (ฮฑ -> ฮฒ -> ฮฒ) -> ฮฒ
-> [ฮฑ] -> ฮฒ foldRight f e [] = e foldRight f e (x:xs) = f x (foldRight f e xs) foldLeft :: (ฮฒ -> ฮฑ -> ฮฒ) -> ฮฒ -> [ฮฑ] -> ฮฒ foldLeft f e [] = e foldLeft f e (x:xs) = foldLeft f (f e x) xs :} > sumLeft = foldLeft (+) 0 > sumRight = foldRight (+) 0 > subLeft = foldLeft (-) 0 > subRight = foldRight (-) 0 > prdLeft = foldLeft (*) 1 > prdRight = foldRight (*) 1 > andLeft = foldLeft (&&) True > andRight = foldRight (&&) True > orLeft = foldLeft (||) False > orRight = foldRight (||) False > concatLeft = foldLeft (++) [] > concatRight = foldRight (++) [] > integers = [1,2,3,4] > flags = [True, False, True] > lists = [[1], [2,3,4],[5,6]] > subLeft(integers) -10 > subRight(integers) -2 > assert (sumLeft(integers) == sumRight(integers)) "OKโ "OK" > assert (subLeft(integers) /= subRight(integers)) "OKโ "OK" > assert (prdLeft(integers) == prdRight(integers)) "OKโ "OK" > assert (andLeft(flags) == andRight(flags)) "OKโ "OK" > assert (orLeft(flags) == orRight(flags)) "OKโ "OK" > assert (concatLeft(lists) == concatRight(lists)) "OKโ "OKโ ๐๐๐๐๐ โ ๐ ๐ฅ๐ = ๐๐๐๐๐ โ ๐ ๐ฅ๐ 1 associative operator unit + 0 * 1 && True || False ++ [] subtraction is not associative, and 0 is not its unit, so the following are not equivalent: foldLeft (-) 0 foldRight (-) 0 where โ and ๐ are such that for all ๐ฅ, ๐ฆ, and ๐ง we have ๐ฅ โ ๐ฆ โ ๐ง = ๐ฅ โ ๐ฆ โ ๐ง ๐ โ ๐ฅ = ๐ฅ and ๐ฅ โ ๐ = ๐ฅ In other words, โ is associative with unit ๐. -
> sumLeft = foldl (+) 0 > sumRight = foldr
(+) 0 > subLeft = foldl (-) 0 > subRight = foldr (-) 0 > prdLeft = foldl (*) 1 > prdRight = foldr (*) 1 > andLeft = foldl (&&) True > andRight = foldr (&&) True > orLeft = foldl (||) False > orRight = foldr (||) False > concatLeft = foldl (++) [] > concatRight = foldr (++) [] > integers = [1,2,3,4] > flags = [True, False, True] > lists = [[1], [2,3,4],[5,6]] > subLeft(integers) -10 > subRight(integers) -2 > assert (sumLeft(integers) == sumRight(integers)) "OKโ "OK" > assert (subLeft(integers) /= subRight(integers)) "OKโ "OK" > assert (prdLeft(integers) == prdRight(integers)) "OKโ "OK" > assert (andLeft(flags) == andRight(flags)) "OKโ "OK" > assert (orLeft(flags) == orRight(flags)) "OKโ "OK" > assert (concatLeft(lists) == concatRight(lists)) "OKโ "OKโ Same as previous slide but using built-in foldl and foldr subtraction is not associative, and 0 is not its unit, so the following are not equivalent: foldl (-) 0 foldr (-) 0 -
def foldr[A, B](f: A => B => B)(e: B)(s: List[A]):
B = s match case Nil => e case x :: xs => f(x)(foldr(f)(e)(xs)) def foldl[A, B](f: B => A => B)(e: B)(s: List[A]): B = s match case Nil => e case x :: xs => foldl(f)(f(e)(x))(xs) val `(+)`: Int => Int => Int = m => n => m + n val `(-)`: Int => Int => Int = m => n => m - n val `(*)`: Int => Int => Int = m => n => m * n val `(&&)`: Boolean => Boolean => Boolean = m => n => m && n val `(||)`: Boolean => Boolean => Boolean = m => n => m || n def `(++)`[A](m: Seq[A]): Seq[A] => Seq[A] = n => m ++ n val sumLeft = foldl(`(+)`)(0) val sumRight = foldr(`(+)`)(0) val subLeft = foldl(`(-)`)(0) val subRight = foldr(`(-)`)(0) val prodLeft = foldl(`(*)`)(1) val prodRight = foldr(`(*)`)(1) val andLeft = foldl(`(&&)`)(true) val andRight = foldr(`(&&)`)(true) val orLeft = foldl(`(||)`)(true) val orRight = foldr(`(||)`)(true) val concatLeft = foldl(`(++)`)(Nil) val concatRight = foldr(`(++)`)(Nil) val integers = List(1, 2, 3, 4) val flags = List(true, false, true) val lists = List(List(1), List(2, 3, 4), List(5, 6)) scala> subLeft(integers) val res0: Int = -10 scala> subRight(integers) val res1: Int = -2 scala> assert( sumLeft(integers) == sumRight(integers) ) | assert( subLeft(integers) != subRight(integers) ) | assert( prodLeft(integers) == prodRight(integers) ) | assert( andLeft(flags) == andRight(flags) ) | assert( orLeft(flags) == orRight(flags) ) | assert( concatLeft(lists) == concatRight(lists) ) scala> subtraction is not associative, and 0 is not its unit, so the following are not equivalent: foldl(`(-)`)(0) foldr(`(-)`)(0) ๐๐๐๐๐ โ ๐ ๐ฅ๐ = ๐๐๐๐๐ โ ๐ ๐ฅ๐ 1 associative operator unit + 0 * 1 && True || False ++ [] where โ and ๐ are such that for all ๐ฅ, ๐ฆ, and ๐ง we have ๐ฅ โ ๐ฆ โ ๐ง = ๐ฅ โ ๐ฆ โ ๐ง ๐ โ ๐ฅ = ๐ฅ and ๐ฅ โ ๐ = ๐ฅ In other words, โ is associative with unit ๐. -
val sumLeft: List[Int] => Int = _.foldLeft(0)(_+_) val sumRight: List[Int]
=> Int = _.foldRight(0)(_+_) val subLeft: List[Int] => Int = _.foldLeft(0)(_-_) val subRight: List[Int] => Int = _.foldRight(0)(_-_) val prodLeft: List[Int] => Int = _.foldLeft(1)(_*_) val prodRight: List[Int] => Int = _.foldRight(1)(_*_) val andLeft: List[Boolean] => Boolean = _.foldLeft(true)(_&&_) val andRight: List[Boolean] => Boolean = _.foldRight(true)(_&&_) val orLeft: List[Boolean] => Boolean = _.foldLeft(false)(_||_) val orRight: List[Boolean] => Boolean = _.foldRight(false)(_||_) def concatLeft[A]: List[List[A]] => List[A] = _.foldLeft(List.empty[A])(_++_) def concatRight[A]: List[List[A]] => List[A] = _.foldRight(List.empty[A])(_++_) val integers = List(1, 2, 3, 4) val flags = List(true, false, true) val lists = List(List(1), List(2, 3, 4), List(5, 6)) scala> subLeft(integers) val res0: Int = -10 scala> subRight(integers) val res1: Int = -2 scala> assert( sumLeft(integers) == sumRight(integers) ) | assert( subLeft(integers) != subRight(integers) ) | assert( prodLeft(integers) == prodRight(integers) ) | assert( andLeft(flags) == andRight(flags) ) | assert( orLeft(flags) == orRight(flags) ) | assert( concatLeft(lists) == concatRight(lists) ) scala> Same as previous slide but using built-in foldLeft and foldRight subtraction is not associative, and 0 is not its unit, so the following are not equivalent: _.foldLeft(0)(_-_) _.foldRight(0)(_-_) -
๐๐๐๐๐ โ ๐ ๐ฅ๐ = ๐๐๐๐๐ โ ๐ ๐ฅ๐ 2
:{ foldRight :: (ฮฑ -> ฮฒ -> ฮฒ) -> ฮฒ -> [ฮฑ] -> ฮฒ foldRight f e [] = e foldRight f e (x:xs) = f x (foldRight f e xs) foldLeft :: (ฮฒ -> ฮฑ -> ฮฒ) -> ฮฒ -> [ฮฑ] -> ฮฒ foldLeft f e [] = e foldLeft f e (x:xs) = foldLeft f (f e x) xs :} > lengthRight = foldr oneplus 0 where oneplus x n = 1 + n > lengthLeft = foldl plusone 0 where plusone n x = n + 1 > assert (lengthRight(list) == lengthLeft(list)) "OK" "OKโ Same as on the left but using built-in foldl and foldr > lengthRight = foldRight oneplus 0 where oneplus x n = 1 + n > lengthLeft = foldLeft plusone 0 where plusone n x = n + 1 > assert (lengthRight(list) == lengthLeft(list)) "OK" "OKโ > reverseRight = foldRight snoc [] where snoc x xs = xs ++ [x] > reverseLeft = foldLeft cons [] where cons xs x = x : xs > assert (reverseRight(list) == reverseLeft(list)) "OK" "OK" > reverseRight = foldr snoc [] where snoc x xs = xs ++ [x] > reverseLeft = foldl cons [] where cons xs x = x : xs > assert (reverseRight(list) == reverseLeft(list)) "OK" "OK" where โ, โ, and ๐ are such that for all ๐ฅ, ๐ฆ, and ๐ง we have ๐ฅ โ ๐ฆ โ ๐ง = ๐ฅ โ ๐ฆ โ ๐ง ๐ฅ โ ๐ = ๐ โ ๐ฅ In other words, โ and โ associate with each other, and ๐ on the right of โ is equivalent to ๐ on the left of โ. list = [1,2,3] -
๐๐๐๐๐ โ ๐ ๐ฅ๐ = ๐๐๐๐๐ โ ๐ ๐ฅ๐ 2
def foldr[A, B](f: A => B => B)(e: B)(s: List[A]): B = s match case Nil => e case x :: xs => f(x)(foldr(f)(e)(xs)) def foldl[A, B](f: B => A => B)(e: B)(s: List[A]): B = s match case Nil => e case x :: xs => foldl(f)(f(e)(x))(xs) def oneplus[A]: A => Int => Int = x => n => 1 + n def plusOne[A]: Int => A => Int = n => x => n + 1 val lengthRight = foldr(oneplus)(0) val lengthLeft = foldl(plusOne)(0) scala> assert( lengthRight(list) == lengthLeft(list) ) def snoc[A]: A => List[A] => List[A] = x => xs => xs ++ List(x) def cons[A]: List[A] => A => List[A] = xs => x => x::xs val reverseRight = foldr(snoc[Int])(Nil) val reverseLeft = foldl(cons[Int])(Nil) scala> assert( reverseRight(list) == reverseLeft(list) ) def oneplus[A]: (A, Int) => Int = (x, n) => 1 + n def plusOne[A]: (Int, A) => Int = (n, x) => n + 1 def lengthRight[A]: List[A] => Int = _.foldRight(0)(oneplus) def lengthLeft[A]: List[A] => Int = _.foldLeft(0)(plusOne) scala> assert( lengthRight(list) == lengthLeft(list) ) def snoc[A]:(A, List[A]) => List[A] = (x, xs) => xs ++ List(x) def cons[A]:(List[A], A) => List[A] = (xs, x) => x::xs def reverseRight[A]: List[A]=>List[A] = _.foldRight(Nil)(snoc) def reverseLeft[A] : List[A]=>List[A] = _.foldLeft(Nil)(cons) scala> assert( reverseRight(list) == reverseLeft(list) ) Same as on the left but using built-in foldLeft and foldRight where โ, โ, and ๐ are such that for all ๐ฅ, ๐ฆ, and ๐ง we have ๐ฅ โ ๐ฆ โ ๐ง = ๐ฅ โ ๐ฆ โ ๐ง ๐ฅ โ ๐ = ๐ โ ๐ฅ In other words, โ and โ associate with each other, and ๐ on the right of โ is equivalent to ๐ on the left of โ. val list: List[Int] = List(1, 2, 3) -
> sumRight = foldRight (+) 0 > sumLeft = foldLeft
(flip (+)) 0 . reverse > assert (sumRight(list) == sumLeft(list)) "OK" "OK" ๐๐๐๐๐ ๐ ๐ ๐ฅ๐ = ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ (๐๐๐ฃ๐๐๐ ๐ ๐ฅ๐ ) 3 :{ foldRight :: (ฮฑ -> ฮฒ -> ฮฒ) -> ฮฒ -> [ฮฑ] -> ฮฒ foldRight f e [] = e foldRight f e (x:xs) = f x (foldRight f e xs) foldLeft :: (ฮฒ -> ฮฑ -> ฮฒ) -> ฮฒ -> [ฮฑ] -> ฮฒ foldLeft f e [] = e foldLeft f e (x:xs) = foldLeft f (f e x) xs :} > sumRight = foldr (+) 0 > sumLeft = foldl (flip (+)) 0 . reverse > assert (sumRight(list) == sumLeft(list)) "OK" "OK" Same as on the left but using built-in foldl and foldr > oneplus x n = 1 + n > lengthRight = foldRight oneplus 0 > lengthLeft = foldLeft (flip oneplus) 0 . reverse > assert (lengthRight(list) == lengthLeft(list)) "OK" "OK" > oneplus x n = 1 + n > lengthRight = foldr oneplus 0 > lengthLeft = foldl (flip oneplus) 0 . reverse > assert (lengthRight(list) == lengthLeft(list)) "OK" "OK" > n โ d = 10 * n + d > decimalLeft = foldLeft (โ) 0 > decimalRight = foldRight (flip (โ)) 0 . reverse > assert (decimalLeft(list) == decimalRight(list)) "OK" > n โ d = 10 * n + d > decimalLeft = foldl (โ) 0 > decimalRight = foldr (flip (โ)) 0 . reverse > assert (decimalLeft(list) == decimalRight(list)) "OK" "OK" (Also holds true when ๐๐๐๐๐ and ๐๐๐๐๐ are swapped) โ โ โ see next slide -
At the bottom of the previous slide and the next
one, instead of exploiting this equation ๐๐๐๐๐ ๐ ๐ ๐ฅ๐ = ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ (๐๐๐ฃ๐๐๐ ๐ ๐ฅ๐ ) we are exploiting the following derived equation in which ๐๐๐๐๐ is renamed to ๐๐๐๐๐ and vice versa: ๐๐๐๐๐ ๐ ๐ ๐ฅ๐ = ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ (๐๐๐ฃ๐๐๐ ๐ ๐ฅ๐ ) The equation can be derived as shown below. Define ๐ = ๐๐๐๐ ๐ and ๐ฆ๐ = ๐๐๐ฃ๐๐๐ ๐ ๐ฅ๐ , from which it follows that ๐ = ๐๐๐๐ ๐ and ๐ฅ๐ = ๐๐๐ฃ๐๐๐ ๐ ๐ฆ๐ . In the original equation, replace ๐ with (๐๐๐๐ ๐) and replace ๐ฅ๐ with (๐๐๐ฃ๐๐๐ ๐ ๐ฆ๐ ) ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ (๐๐๐ฃ๐๐๐ ๐ ๐ฆ๐ ) = ๐๐๐๐๐ ๐๐๐๐ ๐๐๐๐ ๐ ๐ (๐๐๐ฃ๐๐๐ ๐ (๐๐๐ฃ๐๐๐ ๐ ๐ฆ๐ )) Simplify by replacing ๐๐๐๐ ๐๐๐๐ ๐ with ๐ and (๐๐๐ฃ๐๐๐ ๐ (๐๐๐ฃ๐๐๐ ๐ ๐ฆ๐ )) with ๐ฆ๐ ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ ๐๐๐ฃ๐๐๐ ๐ ๐ฆ๐ = ๐๐๐๐๐ ๐ ๐ ๐ฆ๐ Swap the right hand side with left hand side ๐๐๐๐๐ ๐ ๐ ๐ฆ๐ = ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ ๐๐๐ฃ๐๐๐ ๐ ๐ฆ๐ Rename ๐ to ๐ and rename ๐ฆ๐ to ๐ฅ๐ ๐๐๐๐๐ ๐ ๐ ๐ฅ๐ = ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ ๐๐๐ฃ๐๐๐ ๐ ๐ฅ๐ @philip_schwarz -
๐๐๐๐๐ ๐ ๐ ๐ฅ๐ = ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ (๐๐๐ฃ๐๐๐ ๐
๐ฅ๐ ) 3 def flip[A, B, C]: (A => B => C) => (B => A => C) = f => b => a => f(a)(b) val list: List[Int] = List(1, 2, 3) def plus: Int => Int => Int = m => n => m + n val sumRight = foldr(plus)(0) val sumLeft = (xs: List[Int]) => foldl(flip(plus))(0)(xs.reverse) assert( sumRight(list) == sumLeft(list) ) def foldr[A, B](f: A => B => B)(e: B)(s: List[A]): B = s match case Nil => e case x :: xs => f(x)(foldr(f)(e)(xs)) def foldl[A, B](f: B => A => B)(e: B)(s: List[A]): B = s match case Nil => e case x :: xs => foldl(f)(f(e)(x))(xs) def oneplus[A]: A => Int => Int = x => n => 1 + n val lengthRight = foldr(oneplus)(0) def lengthLeft[A] = (xs: List[A]) => foldl(flip(oneplus))(0)(xs.reverse) assert( lengthRight(list) == lengthLeft(list) ) def `(โ)`: Int => Int => Int = n => d => 10 * n + d val decimalLeft = foldl(`(โ)`)(0) val decimalRight = (xs: List[Int]) => foldr(flip(`(โ)`))(0)(xs.reverse) assert( decimalLeft(list) == decimalRight(list) ) โ see previous slide โ -
def flip[A, B, C]: ((A,B) => C) => ((B,A) =>
C) = f => (b,a) => f(a,b) val list: List[Int] = List(1, 2, 3) def plus: (Int,Int) => Int = (m,n) => m + n val sumRight: List[Int] => Int = _.foldRight(0)(plus) val sumLeft: List[Int] => Int = _.reverse.foldLeft(0)(flip(plus)) assert( sumRight(list) == sumLeft(list) ) def oneplus[A]: (A,Int) => Int = (x,n) => 1 + n def lengthRight[A]: List[A] => Int = _.foldRight(0)(oneplus) def lengthLeft[A]: List[A] => Int = _.reverse.foldLeft(0)(flip(oneplus)) assert( lengthRight(list) == lengthLeft(list) ) val `(โ)`: ((Int,Int) => Int) = (n,d) => 10 * n + d val decimalLeft: List[Int] => Int = _.foldLeft(0)(`(โ)`) val decimalRight: List[Int] => Int = _.reverse.foldRight(0)(flip(`(โ)`)) assert( decimalLeft(list) == decimalRight(list) ) Same as previous slide but using built-in foldLeft and foldRight -
In previous slides we saw a decimal function that is
implemented with a right fold. It is derived, using the third duality theorem, from a decimal function implemented with a left fold. 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) > n โ d = 10 * n + d > decimalLeft = foldl (โ) 0 > decimalRight = foldr (flip (โ)) 0 . reverse val `(โ)`: ((Int,Int) => Int) = (n,d) => 10 * n + d val decimalLeft: List[Int] => Int = _.foldLeft(0)(`(โ)`) val decimalRight: List[Int] => Int = _.reverse.foldRight(0)(flip(`(โ)`)) 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) Note how much simpler it is than the decimal function that we came up with in Cheat Sheet #4. That function was produced by the right hand side of the universal property of ๐๐๐๐, after plugging into the left hand side a function that we contrived purely to match that left hand side. -
Cheat Sheet #6 claimed (see bottom of next slide) that
when using Scalaโs built in foldRight function, the reason why doing a right fold over a large collection did not result in a stack overflow error, is that foldRight is defined in terms of foldLeft. -
scala> def foldr[A,B](f: A=>B=>B)(e:B)(s:List[A]):B = | s match { case
Nil => e | case x::xs => f(x)(foldr(f)(e)(xs)) } scala> def `(+)`: Long => Long => Long = | m => n => m + n scala> foldr(`(+)`)(0)(List(1,2,3,4)) val res1: Long = 10 scala> foldr(`(+)`)(0)(List.range(1,10_001)) val res2: Long = 50005000 scala> foldr(`(+)`)(0)(List.range(1,100_001)) java.lang.StackOverflowError scala> // same again but using built-in function scala> List.range(1,10_001).foldRight(0)(_+_) val res3: Int = 50005000 scala> List.range(1,100_001).foldRight(0L)(_+_) val res4: Long = 500000500000 scala> import scala.annotation.tailrec scala> @tailrec | def foldl[A,B](f: B=>A=>B)(e:B)(s:List[A]):B = | s match { case Nil => e | case x::xs => foldl(f)(f(e)(x))(xs) } scala> def `(+)`: Long => Long => Long = | m => n => m + n scala> foldl(`(+)`)(0)(List.range(1,10_001)) val res1: Long = 50005000 scala> foldl(`(+)`)(0)(List.range(1,100_001)) val res2: Long = 5000050000 scala> // same again but using built-in function scala> List.range(1,10_001).foldLeft(0)(_+_) val res3: Int = 50005000 scala> List.range(1,100_001).foldLeft(0L)(_+_) val res4: Long = 5000050000 The reason a stack overflow is not happening here is because built-in foldRight is defined in terms of foldLeft! (see cheat-sheet #7) ๐๐๐๐๐ โท ๐ผ โ ๐ฝ โ ๐ฝ โ ๐ฝ โ ๐ผ โ ๐ฝ ๐๐๐๐๐ ๐ ๐ = ๐ ๐๐๐๐๐ ๐ ๐ ๐ฅ: ๐ฅ๐ = ๐ ๐ฅ ๐๐๐๐๐ ๐ ๐ ๐ฅ๐ ๐๐๐๐๐ โท ๐ฝ โ ๐ผ โ ๐ฝ โ ๐ฝ โ ๐ผ โ ๐ฝ ๐๐๐๐๐ ๐ ๐ = ๐ ๐๐๐๐๐ ๐ ๐ ๐ฅ: ๐ฅ๐ = ๐๐๐๐๐ ๐ ๐ ๐ ๐ฅ ๐ฅ๐ -
The remaining slides provide a justification for that claim, and
are taken from the following deck, which is what this cheat sheet is based on. -
def foldRight[B](z: B)(op: (A, B) => B): B = reversed.foldLeft(z)((b,
a) => op(a, b)) final override def foldRight[B](z: B)(op: (A, B) => B): B = { var acc = z var these: List[A] = reverse while (!these.isEmpty) { acc = op(these.head, acc) these = these.tail } acc } override def foldLeft[B](z: B)(op: (B, A) => B): B = { var acc = z var these: LinearSeq[A] = coll while (!these.isEmpty) { acc = op(acc, these.head) these = these.tail } acc } The reason why doing a right fold over a large collection did not result in a stack overflow error, is that the foldRight function is implemented by code that reverses the sequence, flips the function that it is passed, and then calls foldLeft! While this is not so obvious when we look at the code for foldRight in List, because it effectively inlines the call to foldLeftโฆ โฆit is plain to see in the foldRight function for Seq Third duality theorem. For all finite lists ๐ฅ๐ , ๐๐๐๐๐ ๐ ๐ ๐ฅ๐ = ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ (๐๐๐ฃ๐๐๐ ๐ ๐ฅ๐ ) ๐๐๐๐๐ ๐๐๐๐ ๐ ๐ฅ ๐ฆ = ๐ ๐ฆ ๐ฅ This is the third duality theorem in action -
def foldRight[A,B](as: List[A], z: B)(f: (A, B) => B): B
= as match { case Nil => z case Cons(x, xs) => f(x, foldRight(xs, z)(f)) } Functional Programming in Scala (by Paul Chiusano and Runar Bjarnason) @pchiusano @runarorama sealed trait List[+A] case object Nil extends List[Nothing] case class Cons[+A](head: A, tail: List[A]) extends List[A] def foldRightViaFoldLeft[A,B](l: List[A], z: B)(f: (A,B) => B): B = foldLeft(reverse(l), z)((b,a) => f(a,b)) @annotation.tailrec def foldLeft[A,B](l: List[A], z: B)(f: (B, A) => B): B = l match{ case Nil => z case Cons(h,t) => foldLeft(t, f(z,h))(f) } Implementing foldRight via foldLeft is useful because it lets us implement foldRight tail-recursively, which means it works even for large lists without overflowing the stack. Our implementation of foldRight is not tail-recursive and will result in a StackOverflowError for large lists (we say itโs not stack-safe). Convince yourself that this is the case, and then write another general list- recursion function, foldLeft, that is tail-recursive foldRight(Cons(1, Cons(2, Cons(3, Nil))), 0)((x,y) => x + y) 1 + foldRight(Cons(2, Cons(3, Nil)), 0)((x,y) => x + y) 1 + (2 + foldRight(Cons(3, Nil), 0)((x,y) => x + y)) 1 + (2 + (3 + (foldRight(Nil, 0)((x,y) => x + y)))) 1 + (2 + (3 + (0))) 6 At the bottom of this slide is where Functional Programming in Scala shows that foldRight can be defined in terms of foldLeft. The third duality theorem in action. -
It looks like it was none other than Paul Chiusano
(co-author of FP in Scala), back in 2010, who suggested that Listโs foldRight(z)(f) be implemented as reverse.foldLeft(z)(flip(f))! It also looks like the change was made in 2013 (see next slide) and that it was in 2018 that foldRight was reimplemented as a while loop (see slide after that).