Skip to content

zipWithM

Let's revisit our listDiv function from earlier. Instead of taking a single list of pairs of numbers as input, we now want to take two lists of numbers as input:

GHCi
>>> listDiv [3,1,3,0] [4,2,0,3]
Nothing
>>> listDiv [3,1,0] [4,2,3]
Just [0.75,0.5,0.0]

The first number in the output list produced by listDiv xs ys is the result of dividing the first number in xs by the first number in ys. The second number is the result of dividing the second number in xs by the second number in ys. And so on. If one of the two input lists is shorter, then it is the shorter input list that determines the length of the output, just as with zip and zipWith.

Now, if we wanted to multiply the numbers in the two lists, this could never fail. So we could implement this using zipWith (*):

GHCi
>>> zipWith (*) [3,1,3,0] [4,2,0,3]
[12,2,0,0]
>>> zipWith (*) [3,1,0] [4,2,3]
[12,2,0]

Since division fails when the denominator is 0, we use safeDiv to implement division again, but zipping two lists together using safeDiv produces a list of Maybes:

GHCi
>>> zipWith safeDiv [3,1,3,0] [4,2,0,3]
[Just 0.75,Just 0.5,Nothing,Just 0.0]
>>> zipWith safeDiv [3,1,0] [4,2,3]
[Just 0.75,Just 0.5,Just 0.0]

As before, what we really want is Just a list of numbers if all divisions succeed, and Nothing if there is at least one division that fails. We need to combine the effects of all individual function applications into a single effect with which to decorate the whole list. For our original implementation of listDiv, we did this by replacing map with mapM. Now we replace zipWith with

zipWithM :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m [c]
zipWithM f = go
  where
    go (x:xs) (y:ys) = do
        z  <- f x y
        zs <- go xs ys
        return $ z : zs
    go _ _ = return []

If both lists are non-empty, then go calls f x y to produce the first element z of the output list. It then calls go xs ys recursively to produce the remainder of the output list zs. The result is then z : zs. If at least one of the two input lists is empty, the second equation of go, the result is the empty list, so go _ _ = return []. This sounds exactly like the implementation of zipWith, but the fact that we implement go using do-notation means that we thread the decoration of the monad m through the whole computation. For example, if our monad is the Maybe monad, then the evaluation of f x y may fail, in which case the entire invocation of go fails. A failure of the recursive call go xs ys also makes the entire invocation of go fail. Only if both calls succeed do we obtain the result Just (z : zs) as the output. This is exactly the behaviour of the Maybe monad, which aborts the entire computation if one of the steps in the computation fails.

With zipWithM in hand, the implementation of listDiv becomes really easy:

listDiv :: [Double] -> [Double] -> Maybe [Double]
listDiv = zipWithM safeDiv
GHCi
>>> listDiv = zipWithM safeDiv
>>> listDiv [3,1,3,0] [4,2,0,3]
Nothing
>>> listDiv [3,1,0] [4,2,3]
Just [0.75,0.5,0.0]