- 1. Based on definitions from Left and Right Folds Comparison of a mathematical definition and a programmatic one Polyglot FP for Fun and Profit - Haskell and Scala @philip_schwarz slides by https://www.slideshare.net/pjschwarz Sergei Winitzki sergei-winitzki-11a6431 Richard Bird http://www.cs.ox.ac.uk/people/richard.bird/
- 2. πππππ β· π½ β πΌ β π½ β π½ β πΌ β π½ πππππ π π = π πππππ π π π₯ βΆ π₯π = πππππ π π π π₯ π₯π πππππ β· πΌ β π½ β π½ β π½ β πΌ β π½ πππππ π π = π πππππ π π π₯ βΆ π₯π = π π₯ πππππ π π π₯π If I have to write down the definitions of a left fold and a right fold for lists, here is what I write While both definitions are recursive, the left fold is tail recursive, whereas the right fold isnβt. Although I am very familiar with the above definitions, and view them as doing a good job of explaining the two folds, I am always interested in alternative ways of explaining things, and so I have been looking at Sergei Winitzkiβs mathematical definitions of left and right folds, in his upcoming book: The Science of Functional Programming (SOFP). Sergeiβs definitions of the folds are in the top two rows of the following table Sergei Winitzki Richard Bird @philip_schwarz
- 3. test1 = TestCase (assertEqual "foldl(+) 0 []" 0 (foldl (+) 0 [])) test2 = TestCase (assertEqual "foldl(+) 0 [1,2,3,4]" 10 (foldl (+) 0 [1,2,3,4])) test3 = TestCase (assertEqual "foldr (+) 0 []" 0 (foldr (+) 0 [])) test4 = TestCase (assertEqual "foldr (+) 0 [1,2,3,4]" 10 (foldr (+) 0 [1,2,3,4])) Left folds and right folds do not necessarily produce the same results. According to the first duality theorem of folding, one case in which the results are the same, is when we fold using the unit and associative operation of a monoid. First duality theorem. Suppose β is associative with unit π. Then πππππ β π π₯π = πππππ β π π₯π For all finite lists π₯π . test5 = TestCase (assertEqual "foldl(-) 0 []" 0 (foldl (+) 0 [])) test6 = TestCase (assertEqual "foldl(-) 0 [1,2,3,4]" (- 10) (foldl (-) 0 [1,2,3,4])) test7 = TestCase (assertEqual "foldr (-) 0 []" 0 (foldr (-) 0 [])) test8 = TestCase (assertEqual "foldr (-) 0 [1,2,3,4]" (- 2) (foldr (-) 0 [1,2,3,4])) Folding integers left and right using the (Int,+,0) monoid, for example, produces the same results. But folding integers left and right using subtraction and zero, does not produce the same results, and in fact (Int,-,0) is not a monoid, because subtraction is not associative.
- 4. π = π; π π₯ β§Ί π = π(π₯, π(π )) π = π; π π β§Ί [π₯] = π(π π , π₯) To avoid any confusion (the definitions use the same function name π), and to align with the definitions on the right, letβs modify Sergeiβs definitions by doing some simple renaming. Here are Sergeiβs mathematical definitions again, on the right (the β§Ί operator is list concatenation). Notice how neither definition is tail recursive. That is deliberate. As Sergei explained to me: βI'd like to avoid putting tail recursion into a mathematical formula, because tail recursion is just a detail of how we implement this functionβ and βThe fact that foldLeft is tail recursive for List is an implementation detail that is specific to the List type. It will be different for other sequence types. I do not want to put the implementation details into the formulas.β π β πππππ π β π π β π πππππ = π; πππππ π₯ β§Ί π = π(π₯, πππππ(π )) πππππ = π; πππππ π β§Ί [π₯] = π(πππππ π , π₯) π β πππππ π β π π β π πππππ β· π½ β πΌ β π½ β π½ β πΌ β π½ πππππ π π = π πππππ π π π₯ βΆ π₯π = πππππ π π π π₯ π₯π πππππ β· πΌ β π½ β π½ β π½ β πΌ β π½ πππππ π π = π πππππ π π π₯ βΆ π₯π = π π₯ πππππ π π π₯π π = π; π π₯ β§Ί π = π(π₯, π(π )) π = π; π π β§Ί [π₯] = π(π π , π₯)
- 5. πππππ π π [ ] = π πππππ π π π₯ β§Ί π = π π₯ (πππππ π π π ) πππππ π π = π πππππ π π π β§Ί [π₯] = π πππππ π π π π₯ πππππ :: (π β π β π) β π β[π] β π πππππ π π = π πππππ π π ππ = π πππππ π π (ππππ‘ ππ ) (πππ π‘ ππ ) πππππ :: (π β π β π) β π β[π] β π πππππ π π = π πππππ π π ππ = π βπππ ππ πππππ π π (π‘πππ ππ ) πππππ :: (π β π β π) β π β[π] β π πππππ π π = π πππππ π π ππ = π π π π€βπππ π = πππ π‘ ππ π = πππππ π π (ππππ‘ ππ ) πππππ :: (π β π β π) β π β[π] β π πππππ π π = π πππππ π π ππ = π π π π€βπππ π = βπππ ππ π = πππππ π π (π‘πππ ππ ) πππππ = π; πππππ π₯ β§Ί π = π(π₯, πππππ(π )) πππππ = π; πππππ π β§Ί [π₯] = π(πππππ π , π₯) To help us understand the above two definitions, letβs first express them in Haskell, pretending that we are able to use π β§Ί [π₯] and π₯ β§Ί π in a pattern match. Now letβs replace π β§Ί [π₯] and π₯ β§Ί π with as, and get the functions to extract the π and the π₯ using the βπππ, π‘πππ, πππ π‘ and ππππ‘. Letβs also add type signatures And now letβs make it more obvious that what each fold is doing is taking an π from the list, folding the rest of the list into a π, and then returning the result of calling π with π and π.
- 6. πππππ :: (π β π β π) β π β[π] β π πππππ π π = π πππππ π π ππ = π π π π€βπππ π = πππ π‘ ππ π = πππππ π π (ππππ‘ ππ ) πππππ :: (π β π β π) β π β[π] β π πππππ π π = π πππππ π π ππ = π π π π€βπππ π = βπππ ππ π = πππππ π π (π‘πππ ππ ) Since the above two functions are very similar, letβs extract their common logic into a fold function. Letβs call the function that is used to extract an element from the list π‘πππ, and the function that is used to extract the rest of the list πππ π‘. ππππ :: (π β π β π) β ([π] β π) β ([π] β [π]) β π β [π] β π ππππ π π‘πππ πππ π‘ π = π ππππ π π‘πππ πππ π‘ π ππ = π π π π€βπππ π = π‘πππ ππ π = ππππ π π‘πππ πππ π‘ π (πππ π‘ ππ ) We can now define left and right folds in terms of ππππ. πππππ :: (π β π β π) β π β[π] β π πππππ π = ππππ π πππ π‘ ππππ‘ πππππ :: (π β π β π) β π β[π] β π πππππ π = ππππ ππππ π βπππ π‘πππ ππππ :: (π β π β π) β π β π β π ππππ π π₯ π¦ = π π¦ π₯ The slightly perplexing thing is that while a left fold applies π to list elements starting with the βπππ of the list and proceeding from left to right, the above πππππ function achieves that by navigating through list elements from right to left. Third duality theorem. For all finite lists π₯π , πππππ π π π₯π = πππππ ππππ π π (πππ£πππ π π₯π ) πππππ ππππ π π₯ π¦ = π π¦ π₯ The fact that we can define πππππ and πππππ in terms of ππππ, as we do above, seems related to the third duality theorem of folding. Instead of our πππππ function being passed the reverse of the list passed to πππππ, it processes the list with πππ π‘ and ππππ‘, rather than with βπππ and π‘πππ. @philip_schwarz
- 7. To summarise what we did to help understand SOFPβs mathematical definitions of left and right fold: we turned them into code and expressed them in terms of a common function ππππ that uses a π‘πππ function to extract an element from the list being folded, and a πππ π‘ function to extract the rest of the list. ππππ :: (π β π β π) β ([π] β π) β ([π] β [π]) β π β [π] β π ππππ π π‘πππ πππ π‘ π = π ππππ π π‘πππ πππ π‘ π ππ = π π π π€βπππ π = π‘πππ ππ π = ππππ π π‘πππ πππ π‘ π (πππ π‘ ππ ) πππππ :: (π β π β π) β π β[π] β π πππππ π = ππππ π πππ π‘ ππππ‘ πππππ :: (π β π β π) β π β[π] β π πππππ π = ππππ ππππ π βπππ π‘πππ πππππ = π; πππππ π₯ β§Ί π = π(π₯, πππππ(π )) πππππ = π; πππππ π β§Ί [π₯] = π(πππππ π , π₯) πππππ = π; πππππ ππ = π(βπππ(ππ ), πππππ(π‘πππ(ππ ))) πππππ = π; πππππ ππ = π(πππππ ππππ‘(ππ ) , πππ π‘(ππ )) Letβs feed one aspect of the above back into Sergeiβs definitions. Letβs temporarily rewrite them by replacing π β§Ί [π₯] and π₯ β§Ί π with as, and getting the definitions to extract the π and the π₯ using the functions βπππ, π‘πππ, πππ π‘ and ππππ‘. Notice how the flipping of π done by the πππππ function above, is reflected, in the πππππ function below, in the fact that its π takes an π and a π, rather than a π and an π.
- 8. Another thing we can do to understand SOFPβs definitions of left and right folds, is to see how they work when applied to a sample list, e.g. [π₯0, π₯1, π₯2, π₯3], when we run them manually. In the next slide we first do this for the following definitions πππππ = π; πππππ π β§Ί [π₯] = π(πππππ π , π₯) πππππ = π; πππππ π₯ β§Ί π = π(π₯, πππππ(π )) πππππ β· π½ β πΌ β π½ β π½ β πΌ β π½ πππππ π π = π πππππ π π π₯ βΆ π₯π = πππππ π π π π₯ π₯π πππππ β· πΌ β π½ β π½ β π½ β πΌ β π½ πππππ π π = π πππππ π π π₯ βΆ π₯π = π π₯ πππππ π π π₯π In the slide after that, we do it for SOFPβs definitions.
- 9. βΆ / π₯0 βΆ / π₯1 βΆ / π₯2 βΆ / π₯3 π / π π₯3 / π π₯2 / π π₯1 / π π₯0 π / π₯0 π / π₯1 π / π₯2 π / π₯3 π π₯0: (π₯1: π₯2: π₯3: ) π π π π π π₯0 π₯1 π₯2 π₯3 var πππ = π foreach(π₯ in π₯s) πππ = π(πππ, π₯) return πππ πππππ π π π₯π πππππ π π π₯π π₯π = [π₯0, π₯1, π₯2, π₯3] πππππ β· πΌ β π½ β π½ β π½ β πΌ β π½ πππππ π π = π πππππ π π π₯: π₯π = π π₯ πππππ π π π₯π πππππ β· π½ β πΌ β π½ β π½ β πΌ β π½ πππππ π π = π πππππ π π π₯: π₯π = πππππ π π π π₯ π₯π πππππππ: βΆ π€ππ‘β π π€ππ‘β π πππππ π π [π₯0, π₯1, π₯2, π₯3] π π₯0 πππππ π π [π₯1, π₯2, π₯3] π π₯0 (π π₯1 (πππππ π π [π₯2, π₯3])) π π₯0 (π π₯1 (π π₯2 (πππππ π π [π₯3]))) π π₯0 (π π₯1 (π π₯2 (π π₯3 (πππππ π π [ ])))) π π₯0 (π π₯1 (π π₯2 (π π₯3 π))) π π₯0 (π π₯1 (π π₯2 (π π₯3 π))) πππππ π π [π₯0, π₯1, π₯2, π₯3] πππππ π π π π₯0 [π₯1, π₯2, π₯3] πππππ π π π π π₯0 π₯1 [π₯2, π₯3] πππππ π π π π π π₯0 π₯1 π₯2 [π₯3] πππππ π π π π π π π₯0 π₯1 π₯2 π₯3 [ ] π π π π π π₯0 π₯1 π₯2 π₯3
- 10. βΆ / π₯0 βΆ / π₯1 βΆ / π₯2 βΆ / π₯3 π / π π₯3 / π π₯2 / π π₯1 / π π₯0 π / π₯0 π / π₯1 π / π₯2 π / π₯3 π π₯0: (π₯1: π₯2: π₯3: ) π(π π π π, π₯0 , π₯1 , π₯2 , π₯3) πππππ π₯π πππππ π₯π π₯π = [π₯0, π₯1, π₯2, π₯3] π(π₯0, π(π₯1, π(π₯2, π(π₯3, π)))) πππππ = π; πππππ π β§Ί [π₯] = π(πππππ π , π₯) πππππ = π; πππππ π₯ β§Ί π = π(π₯, πππππ(π )) πππππ π₯0, π₯1, π₯2, π₯3 , π πππππ π₯0, π₯1, π₯2 , π₯3 π(π(πππππ [π₯0, π₯1] , π₯2), π₯3) π π π πππππ π₯0 , π₯1 , π₯2 , π₯3 π π π π πππππ [ ] , π₯0 , π₯1 , π₯2 , π₯3 π π π π π, π₯0 , π₯1 , π₯2 , π₯3 πππππ([π₯0, π₯1, π₯2, π₯3]) π(π₯0, πππππ([π₯1, π₯2, π₯3])) π(π₯0, π(π₯1, πππππ([π₯2, π₯3]))) π(π₯0, π(π₯1, π(π₯2, πππππ([π₯3])))) π(π₯0, π(π₯1, π(π₯2, π(π₯3, πππππ([]))))) π(π₯0, π(π₯1, π(π₯2, π(π₯3, π))))
- 11. πππππ β· π½ β πΌ β π½ β π½ β πΌ β π½ πππππ π π = π πππππ π π π₯: π₯π = πππππ π π π π₯ π₯π πππππ = π; πππππ π β§Ί [π₯] = π(πππππ π , π₯) βΆ / π₯0 βΆ / π₯1 βΆ / π₯2 βΆ / π₯3 π₯0: (π₯1: π₯2: π₯3: ) π₯π = [π₯0, π₯1, π₯2, π₯3] π / π π₯3 / π π₯2 / π π₯1 / π π₯0 πππππ π π [π₯0, π₯1, π₯2, π₯3] πππππ π π π π₯0 [π₯1, π₯2, π₯3] πππππ π π π π π₯0 π₯1 [π₯2, π₯3] πππππ π π π π π π₯0 π₯1 π₯2 [π₯3] πππππ π π π π π π π₯0 π₯1 π₯2 π₯3 [ ] π π π π π π₯0 π₯1 π₯2 π₯3 πππππ π₯0, π₯1, π₯2, π₯3 , π πππππ π₯0, π₯1, π₯2 , π₯3 π(π(πππππ [π₯0, π₯1] , π₯2), π₯3) π π π πππππ π₯0 , π₯1 , π₯2 , π₯3 π π π π πππππ [ ] , π₯0 , π₯1 , π₯2 , π₯3 π π π π π, π₯0 , π₯1 , π₯2 , π₯3 Now letβs compare the results for both definitions of πππππ. The results are the same. @philip_schwarz
- 12. π / π₯0 π / π₯1 π / π₯2 π / π₯3 π πππππ β· πΌ β π½ β π½ β π½ β πΌ β π½ πππππ π π = π πππππ π π π₯: π₯π = π π₯ πππππ π π π₯π πππππ π π [π₯0, π₯1, π₯2, π₯3] π π₯0 πππππ π π [π₯1, π₯2, π₯3] π π₯0 (π π₯1 (πππππ π π [π₯2, π₯3])) π π₯0 (π π₯1 (π π₯2 (πππππ π π [π₯3]))) π π₯0 (π π₯1 (π π₯2 (π π₯3 (πππππ π π [ ])))) π π₯0 (π π₯1 (π π₯2 (π π₯3 π))) πππππ = π; πππππ π₯ β§Ί π = π(π₯, πππππ(π )) πππππ([π₯0, π₯1, π₯2, π₯3]) π(π₯0, πππππ([π₯1, π₯2, π₯3])) π(π₯0, π(π₯1, πππππ([π₯2, π₯3]))) π(π₯0, π(π₯1, π(π₯2, πππππ([π₯3])))) π(π₯0, π(π₯1, π(π₯2, π(π₯3, πππππ([]))))) π(π₯0, π(π₯1, π(π₯2, π(π₯3, π)))) βΆ / π₯0 βΆ / π₯1 βΆ / π₯2 βΆ / π₯3 π₯0: (π₯1: π₯2: π₯3: ) π₯π = [π₯0, π₯1, π₯2, π₯3] And now letβs compare the results for both definitions of πππππ. Again, the results are the same.
- 13. The way πππππ applies π to π₯, π¦, z is by associating to the right. The way πππππ does it is by associating to the right. Thinking of π as an infix operator can help to grasp this. πππππ π(π₯, π(π¦, π§)) π₯ β π¦ β π§ πππππ π π π₯, π¦ , π§ (π₯ β π¦) β π§ πππππ β π π₯, π¦, z = π₯ β (π¦ β π§ β π ) πππππ (+) 0 [1,2,3] = 1 + (2 + (3 + 0)) = 6 πππππ (+) 0 [1,2,3] = ((0 + 1) + 2) + 3 = 6 πππππ β 0 1,2,3 = 1 β (2 β (3 β 0)) = 2 πππππ β 0 1,2,3 = ((0 β 1) β 2) β 3 = β6 β / π₯ β / π¦ β / π§ π β / β π§ / β π¦ / π π₯ πππππ β π π₯, π¦, π§ = ((π β π₯) β π¦) β π§ Addition is associative, so associating to the left and to the right yields the same result. But subtraction isnβt associative, so associating to the left yields a result that is different to the one yielded when associating to the right.
- 14. We have seen that Sergeiβs definitions of left and right folds make perfect sense. Not only are they simple and succinct, they are also free of implementation details like tail recursion. Thatβs all. I hope you found this slide deck useful. By the way, in case you are interested, see below for a whole series of slide decks dedicated to folding.