Learn You a Haskell for Great Good! - Miran Lipovaca [29]
Implementing zipWith
Now we’re going to use higher-order programming to implement a really useful function in the standard library called zipWith. It takes a function and two lists as parameters, and then joins the two lists by applying the function between corresponding elements. Here’s how we’ll implement it:
zipWith' :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith' _ [] _ = []
zipWith' _ _ [] = []
zipWith' f (x:xs) (y:ys) = f x y : zipWith' f xs ys
First let’s look at the type declaration. The first parameter is a function that takes two arguments and returns one value. They don’t have to be of the same type, but they can be. The second and third parameters are lists, and the final return value is also a list.
The first list must be a list of type a values, because the joining function takes a types as its first argument. The second must be a list of b types, because the second parameter of the joining function is of type b. The result is a list of type c elements.
Note
Remember that if you’re writing a function (especially a higher-order function), and you’re unsure of the type, you can try omitting the type declaration and checking what Haskell infers it to be by using :t.
This function is similar to the normal zip function. The base cases are the same, although there’s an extra argument (the joining function). However, that argument doesn’t matter in the base cases, so we can just use the _ character for it. The function body in the last pattern is also similar to zip, though instead of doing (x, y), it does f x y.
Here’s a little demonstration of all the different things our zipWith' function can do:
ghci> zipWith' (+) [4,2,5,6] [2,6,2,3]
[6,8,7,9]
ghci> zipWith' max [6,3,2,1] [7,3,1,5]
[7,3,2,5]
ghci> zipWith' (++) ["foo ", "bar ", "baz "] ["fighters", "hoppers", "aldrin"]
["foo fighters","bar hoppers","baz aldrin"]
ghci> zipWith' (*) (replicate 5 2) [1..]
[2,4,6,8,10]
ghci> zipWith' (zipWith' (*)) [[1,2,3],[3,5,6],[2,3,4]] [[3,2,2],[3,4,5],[5,4,3]]
[[3,4,6],[9,20,30],[10,12,12]]
As you can see, a single higher-order function can be used in very versatile ways.
Implementing flip
Now we’ll implement another function in the standard library, called flip. The flip function takes a function and returns a function that is like our original function, but with the first two arguments flipped. We can implement it like this:
flip' :: (a -> b -> c) -> (b -> a -> c)
flip' f = g
where g x y = f y x
You can see from the type declaration that flip' takes a function that takes a and b types, and returns a function that takes b and a types. But because functions are curried by default, the second pair of parentheses actually is not necessary. The arrow -> is right-associative by default, so (a -> b -> c) -> (b -> a -> c) is the same as (a -> b -> c) -> (b -> (a -> c)), which is the same as (a -> b -> c) -> b -> a -> c. We wrote that g x y = f y x. If that’s true, then f y x = g x y must also hold, right? Keeping that in mind, we can define this function in an even simpler manner:
flip' :: (a -> b -> c) -> b -> a -> c
flip' f y x = f x y
In this new version of flip', we take advantage of the fact that functions are curried. When we call flip' f without the parameters y and x, it will return an f that takes those two parameters but calls them flipped.
Even though flipped functions are usually passed to other functions, we can take advantage of currying when making higher-order functions by thinking ahead and writing what their end result would be if they were fully applied.
ghci> zip [1,2,3,4,5] "hello"
[(1,'h'),(2,'e'),(3,'l'),(4,'l'),(5,'o')]
ghci> flip' zip [1,2,3,4,5] "hello"
[('h',1),('e',2),('l',3),('l',4),('o',5)]
ghci> zipWith div [2,2..] [10,8,6,4,2]