Skip to content

Instantly share code, notes, and snippets.

@regiskuckaertz
Last active July 27, 2018 13:42
Show Gist options
  • Save regiskuckaertz/83135733a7b93bd2f2e5c54b3e2a92e5 to your computer and use it in GitHub Desktop.
Save regiskuckaertz/83135733a7b93bd2f2e5c54b3e2a92e5 to your computer and use it in GitHub Desktop.
Practicing derivations: example #1

Thirty years ago, Jon Bentley wrote "Programming Pearls", a collection of great (imperative) algorithms. One of them was to find the maximum segment sum:

Given a list of numbers, the task is to compute the largest possible sum of a consecutive segment.

The "pearls" schtick caught on in the FP community, which came up with a research paper (of course) called "Functional pearls". In his book "Pearls of functional algorithm design", Richard Bird calculates an efficient version by derivation.

It starts with the specification:

mss :: [Int] -> Int
mss = max . map sum . segments

segments :: [Int] -> [[Int]]
segments = concat . map inits . tails

tails :: [a] -> [[a]]
tails [] = [[]]
tails (x:xs) = (x:xs) : tails xs

inits :: [a] -> [[a]]
inits xs = foldr (\x xss -> [] : map (x:) xss) [[]] xs

Imperative programming aficionados would laugh at this O(n^3) algorithm and walk away. Functional programmers have laws 😁

Let's look where we can go from here:

max . map sum . segments
= {- def of segments -}
max . map sum . concat . map inits . tails
= {- map f . concat = concat . map (map f) -}
max . concat . map (map sum) . map inits . tails
= {- max . concat = max . map max -}
max . map max . map (map sum) . map inits . tails
= {- functor law -}
max . map (max . map sum . inits) . tails

Nice! Notice that inits is a fold, so we can use the fold fusion law twice. That's our plan of attack! Let's focus on the inner expression first:

max . map sum . inits
= {- def of inits -}
max . map sum . foldr (\x xss -> [] : map (x:) xss) [[]]
= {- fusion law, b = g a = map sum [[]] = [0], f (g x y) = h x (g y) = zsum x (map sum y) -}
max . foldr zsum [0]

Of course we need to derive zsum 😅 Let's do that. All we need is start from the f (g x y) expression and derive the hell out of it.

map sum ([] : map (x:) xss)
= {- def of map -}
sum [] : map sum . (map (x:)) xss 
= {- functor law, sum [] = 0 -}
0 : map (sum . (x:)) xss
= {- sum . (x:) xs = sum x:xs = x + sum xs -}
0 : map (x+) (map sum xss)
= {- introduce zsum x xs = 0 : map (x+) xs -}
zsum x (map sum xss)

Good. Let's do the same exercise with max . foldr zsum [0]:

max . foldr zsum [0]
= {- fusion law, max [0] = 0, introduce zmax x (max y) -}
foldr zmax 0

The derivation of zmax is a piece of cake:

max (0 : map (x+) xss)
= {- max (0:xs) = 0 `maximum` max xs, where maximum is the binary version of max -} 
0 `maximum` max (map (x+) xss)
= {- max (map (x+)) xss = x + max xss -}
0 `maximum` x + max xss
= {- introduce zmax x y = 0 `maximum` x + y -}
zmax x (max xss)

We've already reduced the complexity by arriving at mss = max . map (foldr zmax 0) . tails. Additionally, the expression is an instance of the scan lemma, i.e. map (foldr f a) . tails = scanr f a. We can thus write

mss = max . scanr zmax 0

This version is linear in complexity. Some improvement is possible, namely reducing the space complexity to O(1), using the so-called "tupling method". We can look into it another time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment