zip, zipWith, and unzip
It is not uncommon to want to combine multiple lists into a single list, pairing the first element in one list with the first element in another list, and the same for the second element, third element, and so on:
>>> zip [1,2,3] ["Hello","you","there"]
[(1,"Hello"),(2,"you"),(3,"there")]
We may also want to take a list of pairs apart into two lists:
>>> unzip [(1,"Hello"),(2,"you"),(3,"there")]
([1,2,3],["Hello","you","there"])
zip
and unzip
are the functions for the job.
zip :: [a] -> [b] -> [(a,b)]
zip (x:xs) (y:ys) = (x,y) : zip xs ys
zip _ _ = []
unzip :: [(a,b)] -> ([a], [b])
unzip [] = ([], [])
unzip ((x,y):xys) = (x:xs, y:ys)
where
(xs, ys) = unzip xys
If you squint at this implementation of zip
, you will see that the list it
produces has the length of the shorter of the two input lists. The remaining
elements in the longer list are discarded:
>>> zip [1,2,3] "Hello"
[(1,'H'),(2,'e'),(3,'l')]
This makes perfect sense: Since the output list needs to contain pairs of elements from the two input lists, what should we pair an element in the longer list with if there are no more elements in the shorter list left?
Combined with lazy evaluation (see previous chapter), this behaviour is tremendously useful.
zip
also has a more general sibling,
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f = go
where
go (x:xs) (y:ys) = f x y : go xs ys
go _ _ = []
Instead of forming pairs of elements taken from the two input lists, zipWith
uses the provided function to compute a value of an arbitrary type c
from each
pair of elements taken from the two input lists. For example,
>>> zipWith (+) [1,2,3] [4,5,6]
[5,7,9]
Since (,) :: a -> b -> (a, b)
is a data constructor, we have
zip = zipWith (,)
Conversely,
zipWith f xs ys = map (uncurry f) $ zip xs ys
>>> map (uncurry (+)) $ zip [1,2,3] [4,5,6]
[5,7,9]
I'll leave it as an exercise to figure out that this relationship holds. As a bonus exercise, figure out why we weren't able to define
zipWith f = map (uncurry f) . zip
As a final comment, the Data.List
module also has versions of zip
, unzip
,
and zipWith
that take more than two lists as arguments or, in the case of
unzip
, produce more than two lists. These functions are called zip3
,
unzip3
, zipWith3
, zip4
, unzip4
, zipWith4`, and so on. For example,
>>> zip3 [1,2,3] [False,True,False] ["Hello","beautiful","world"]
[(1,False,"Hello"),(2,True,"beautiful"),(3,False,"world")]
Exercise
Use zip
, filter
, and map
to implement a function
everyOther :: [a] -> [a]
that returns a list containing every other element of its argument list:
>>> everyOther [1,2,3,4,5]
[1,3,5]
>>> everyOther "Hello"
"Hlo"
To do so elegantly, you'll find the infinite list of integers [1..]
helpful.
Solution
everyOther = map snd . filter (odd . fst) . zip [1..]
everyOther xs
applies zip [1..]
to xs
, that is, it pairs every
element of xs
with its position in the list:
>>> zip [1..] "Hello"
[(1,'H'),(2,'e'),(3,'l'),(4,'l'),(5,'o')]
It then keeps only those pairs that satisfy the condition odd . fst
,
that is, those pairs corresponding to elements at odd positions in xs
:
>>> filter (odd . fst) $ zip [1..] "Hello"
[(1,'H'),(3,'l'),(5,'o')]
And finally, it strips the positions of the remaining elements, that is, it produces a new list containing only the second component of each pair. We used the positions only to figure out which elements to keep:
>>> map snd $ filter (odd . fst) $ zip [1..] "Hello"
"Hlo"
So this gives
everyOther xs = map snd $ filter (odd . fst) $ zip [1..] xs
The above is just the point-free form that uses function composition. If you don't see why these two definitions are equivalent, here is how we arrive at the point-free form:
everyOther xs
= map snd $ filter (odd . fst) $ zip [1..] xs
= map snd (filter (odd . fst) (zip [1..] xs)) -- Definition of ($)
= (map snd . filter (odd . fst)) (zip [1..] xs) -- Definition of (.)
= (map snd . filter (odd . fst) . zip [1..]) xs -- Definition of (.)
everyOther = map snd . filter (odd . fst) . zip [1..] -- Drop the common argument xs from both sides
Exercise
Implement a function
filterUps :: Ord a => [a] -> [a]
Given an input list xs
, it keeps only those elements in xs
that are
greater than the elements right before them:
>>> filterUps []
[]
>>> filterUps [1]
[]
>>> filterUps [1,2,4,3,7,10,5,4,5]
[2,4,7,10,5]
In a list with less than two elements, there is no element that has any element before it that could be smaller. So the result is the empty list. In the last example, 1 has no predecessor, so it is discarded. 2, 4 (the first one), 7, 10, and 5 (the second one) are greater than the elements before them, so they are kept. 3, 5 (the first one), and 4 (the second one) are less than the elements before them, so they are discarded.
You'll likely need zip
, filter
, and map
to implement this function.
Solution
filterUps :: Ord a => [a] -> [a]
filterUps [] = []
filterUps xs = map snd $ filter (uncurry (<)) $ zip xs (tail xs)
Zipping xs
with tail xs
produces a list of all pairs of consecutive
elements in xs
:
>>> xs = [1,2,4,3,7,10,5,4,5]
>>> zip xs (tail xs)
[(1,2),(2,4),(4,3),(3,7),(7,10),(10,5),(5,4),(4,5)]
filter (uncurry (<))
keeps only those pairs where the first element is
greater than the second one:
>>> filter (uncurry (<)) $ zip xs (tail xs)
[(1,2),(2,4),(3,7),(7,10),(4,5)]
Those are exactly the pairs whose second components are the elements of xs
that should be kept. map snd
extracts these second components:
>>> map snd $ filter (uncurry (<)) $ zip xs (tail xs)
[2,4,7,10,5]