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:
>>> 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 (*)
:
>>> 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 Maybe
s:
>>> 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
>>> 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]