Exercise solutions

Exercise 1

Try implementing liftA2 for Maybe.

The type looks like this.

liftA2 @Maybe :: (a -> b -> c) ->
                 Maybe a -> Maybe b -> Maybe c

As we said, it’s very close to fmap which you have implemented before. We’ll call the function argument f, but you might have called it something else.

liftA2 @Maybe :: (a -> b -> c) ->
                 Maybe a -> Maybe b -> Maybe c
liftA2 f _ Nothing = Nothing
liftA2 f Nothing _ = Nothing
liftA2 f (Just x) (Just y) = Just (f x y)

Here’s another flavor – semantically the same, syntactically different.

liftCase :: (a -> b -> c) ->
            Maybe a -> Maybe b -> Maybe c
liftCase f x y =
  case x of
    Nothing -> Nothing
    Just x ->
      case y of
        Nothing -> Nothing
        Just y -> Just (f x y)

You could, of course, write a little property test to make sure that it’s behaving the same as liftA2 @Maybe from base, but that’s probably not necessary.

Exercise 2

Given the similarities with Functor, see if you can write an instance of Applicative for Either. You can implement either (<*>) or liftA2 or both. You may want to un-import Either and its constructors from Prelude. You’ll need to have a Functor instance in scope for Either.

Remember you have a choice of implementing the tie-fighter or liftA2 for your instance, but you must have pure either way. We’ve used the Either names for our instances here (un-importing them from Prelude).

The types of the (<*>) and liftA2 as specialized to Either a are as follows:

λ> :type (<*>) @(Either _)
(<*>) @(Either _)
  :: Either w (a -> b) -> Either w a -> Either w b

λ> :type liftA2 @(Either _)
liftA2 @(Either _)
  :: (a -> b -> c) ->
     Either w a -> Either w b -> Either w c

Left values, which contain the w, are treated the same as Nothing in the Maybe instances.

instance Applicative (Either a) where
  pure = Right
  Left x <*> _ = Left x
  _ <*> Left y = Left y
  Right f <*> Right y = Right (f y)

Alternatively:

instance Applicative (Either a) where
  pure = Right
  liftA2 f (Left x) _ = Left x
  liftA2 f _ (Left y) = Left y
  liftA2 f (Right x) (Right y) = Right (f x y)

You should be noticing a pattern at this point, not just with these but a commonality with the structure of Functor instances, too. There is usually only one sensible implementation for a given typeFor Functor there is only ever one sensible implementation, or so we’re told – we’ve never proven this ourselves. Therefore, there is a language extension you can use to derive Functor instances, much as you derive Show and others. For now, we’d encourage you to keep writing instances by hand as long as you can stand it. The day you find yourself thinking how trivial it is to write this next Functor instance, it’s probably safe to start deriving them., and you just do the simplest thing. Part of the reason we have you write so many instances – and you should be writing even more! – is because doing these until they are mechanical because you’ve internalized the pattern is the best way we know to make these patterns really useful to you.

Sign up for access to the full page, plus the complete archive and all the latest content.