I tried to understand this elegant solution myself, so I tried to derive the types and evaluation myself. So, we need to write a function:
zip xs ys = foldr step done xs ys
Here we need to derive step
and done
, whatever they are. Recall foldr
's type, instantiated to lists:
foldr :: (a -> state -> state) -> state -> [a] -> state
However our foldr
invocation must be instantiated to something like below, because we must accept not one, but two list arguments:
foldr :: (a -> ? -> ?) -> ? -> [a] -> [b] -> [(a,b)]
Because ->
is right-associative, this is equivalent to:
foldr :: (a -> ? -> ?) -> ? -> [a] -> ([b] -> [(a,b)])
Our ([b] -> [(a,b)])
corresponds to state
type variable in the original foldr
type signature, therefore we must replace every occurrence of state
with it:
foldr :: (a -> ([b] -> [(a,b)]) -> ([b] -> [(a,b)]))
-> ([b] -> [(a,b)])
-> [a]
-> ([b] -> [(a,b)])
This means that arguments that we pass to foldr
must have the following types:
step :: a -> ([b] -> [(a,b)]) -> [b] -> [(a,b)]
done :: [b] -> [(a,b)]
xs :: [a]
ys :: [b]
Recall that foldr (+) 0 [1,2,3]
expands to:
1 + (2 + (3 + 0))
Therefore if xs = [1,2,3]
and ys = [4,5,6,7]
, our foldr
invocation would expand to:
1 `step` (2 `step` (3 `step` done)) $ [4,5,6,7]
This means that our 1 `step` (2 `step` (3 `step` done))
construct must create a recursive function that would go through [4,5,6,7]
and zip up the elements. (Keep in mind, that if one of the original lists is longer, the excess values are thrown away). IOW, our construct must have the type [b] -> [(a,b)]
.
3 `step` done
is our base case, where done
is an initial value, like 0
in foldr (+) 0 [1..3]
. We don't want to zip anything after 3, because 3 is the final value of xs
, so we must terminate the recursion. How do you terminate the recursion over list in the base case? You return empty list []
. But recall done
type signature:
done :: [b] -> [(a,b)]
Therefore we can't return just []
, we must return a function that would ignore whatever it receives. Therefore use const
:
done = const [] -- this is equivalent to done = \_ -> []
Now let's start figuring out what step
should be. It combines a value of type a
with a function of type [b] -> [(a,b)]
and returns a function of type [b] -> [(a,b)]
.
In 3 `step` done
, we know that the result value that would later go to our zipped list must be (3,6)
(knowing from original xs
and ys
). Therefore 3 `step` done
must evaluate into:
\(y:ys) -> (3,y) : done ys
Remember, we must return a function, inside which we somehow zip up the elements, the above code is what makes sense and typechecks.
Now that we assumed how exactly step
should evaluate, let's continue the evaluation. Here's how all reduction steps in our foldr
evaluation look like:
3 `step` done -- becomes
(\(y:ys) -> (3,y) : done ys)
2 `step` (\(y:ys) -> (3,y) : done ys) -- becomes
(\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys)
1 `step` (\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys) -- becomes
(\(y:ys) -> (1,y) : (\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys) ys)
The evaluation gives rise to this implementation of step (note that we account for ys
running out of elements early by returning an empty list):
step x f = \[] -> []
step x f = \(y:ys) -> (x,y) : f ys
Thus, the full function zip
is implemented as follows:
zip :: [a] -> [b] -> [(a,b)]
zip xs ys = foldr step done xs ys
where done = const []
step x f [] = []
step x f (y:ys) = (x,y) : f ys
P.S.: If you are inspired by elegance of folds, read Writing foldl using foldr and then Graham Hutton's A tutorial on the universality and expressiveness of fold.