Lecture 8 — 2020-02-13

Applicative

This lecture is written in literate Haskell; you can download the raw source.

A lot of this material is borrowed/based on two resources written by Brent Yorgey: his Haskell course from Spring 2013 and his Typeclassopedia. I encourage you to go read these excellent resources!

A problem in Functortown

We looked at how we might define arithmetic evaluation with the possibility of errors:

evalA' :: Store -> AExp -> Either Error Int`
evalA' st (Neg e) = myNegate $ evalA' st e
  where myNegate (Left err) = Left err
        myNegate (Right n) = Right $ negate n

We observed that myNegate = fmap negate, so we can instead write:

evalA' :: Store -> AExp -> Either Error Int`
evalA' st (Neg e) = negate <$> evalA' st e

(Recall that (<$>) is the same thing as fmap, but infix—it’s deliberately designed to look like application, ($).)

We then tried to work out the case for Plus:

evalA' :: Store -> AExp -> Either Error Int`
evalA' st (Neg e) = negate <$> evalA' st e
evalA' st (Plus e1 e2) = fmap2 (+) (evalA' st e1) (evalA' st e2)

What would this function fmap2 be? For our concrete example, we succeeded in defining a function:

fmap2 :: (a -> b -> c) -> Either e a -> Either e b -> Either e c
fmap2 _ (Left err1) _          = Left err1
fmap2 _ _          (Left err2) = Left err2
fmap2 f (Right v1) (Right v2)  = Right $ f v1 v2

But can we go more general? We tried writing a different function fmap2, but that didn’t go well either.

fmap2 :: Functor f => (a -> b -> c) -> f a -> f b -> f c
fmap2 f a b = undefined
  where fa = fmap f a -- where do we go from here?

Functor is a fine type class, but it only lets us operate opaquely. Once a function is trapped in Functor, we can’t get it out!

A different type class, Applicative, solves this problem.

class Functor f => Applicative f where
  pure  :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b -- pronounced 'ap'

NB that Applicative is a subclass of Functor.

fmap2' :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
fmap2' f a b = (fmap f a) <*> b
pure  :: a             -> f a
fmap  :: (a -> b)      -> f a -> f b
fmap2 :: (a -> b -> c) -> f a -> f b -> f c

Note that to be an Applicative, you must be a Functor. Can we write fmap using the <*>?

fmap' :: Applicative f => (a -> b) -> f a -> f b
fmap' f a = pure f <*> a

In fact, using fmap with Applicatives is so common that we have the definition <$> = fmap.

fmap2Best :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
fmap2Best f a b = f <$> a <*> b -- note left associativity and tight binding of <$>

Finally, note that fmap2 is called liftA2 in Haskell.

Obey the laws

Like Functor, the Applicative type class is governed by laws.

Identity: pure id <*> v = v Composition: pure (.) <*> u <*> v <*> w = u <*> (v <*> w) Homomorphism: pure f <*> pure x = pure (f x) Interchange: u <*> pure y = pure ($ y) <*> u

Note that identity is a generalization of id <$> v = v from Functor, since f <$> x = pure f <*> x.

Another way in

type Name = String

data Employee = Employee { name    :: Name
                         , phone   :: String }
                deriving Show

maybeEmployee :: (Name -> String -> Employee) ->
                 (Maybe Name -> Maybe String -> Maybe Employee)
maybeEmployee f (Just n) (Just p) = Just $ f n p
maybeEmployee _ _ _ = Nothing

listEmployee :: (Name -> String -> Employee) ->
                ([Name] -> [String] -> [Employee])
listEmployee f n p = zipWith f n p

listEmployee' :: (Name -> String -> Employee) ->
                 ([Name] -> [String] -> [Employee])
listEmployee' f n p = map (\(name,phone) -> f name phone) allPairs
  where allPairs = Prelude.concat (map (\name -> map (\phone -> (name,phone)) p) n)
  -- map (uncurry f) $ concatMap (\name -> map (\phone -> (name,phone)) p) n

funEmployee :: (Name -> String -> Employee) ->
               (e -> Name) -> (e -> String) -> (e -> Employee)
funEmployee f mkName mkPhone = \e -> f (mkName e) (mkPhone e)

Another example of Applicative, highlighting how it bears some similarity to normal “application”:

name :: Maybe String -> Maybe String -> Maybe String
name given family = (++) <$> given <*> family

drdave = name (Just "Dave") (Just "Kauchak")

prince = name (Just "Prince") Nothing

We wrote up a straightforward instance for Maybe and a more interesting instance for Either e:

instance Applicative (Either e) where
  pure x = Right x -- because Left x would be ill typed!
  (Right f) <*> (Right v) = Right $ f v
  err@(Left e) <*> _ = err
  _ <*> err@(Left e) = err

Then we (very quickly, at the end) went over the Applicative definitions for lists. There were two possibilities: Cartesian product…

instance Applicative [] where
  pure x = [x]

  []     <*>  _ = []
  _      <*> [] = []
  (f:fs) <*> xs = map f xs ++ fs <*> xs

…and zipping:

newtype ZipList a = ZipList { getZipList :: [a] }
  deriving (Eq, Show, Functor)

instance Applicative ZipList where
  pure = ZipList . repeat
  ZipList fs <*> ZipList xs = ZipList (zipWith ($) fs xs)

(We wrote the infinite list differently, writing pure x = x:pure x. That’s not quite right, because we want an infinite ZipList, not an infinite list. The above definition is the technically correct one.)

Haskell prefers the former definition based on Cartesian product, because it doesn’t involve any infinite lists. Infinite lists are fine in Haskell, because Haskell is lazy… but even so, it’s very easy to accidentally use an infinite list to create an infinite loop. (For example, a map is fine, but a fold? Not always…)