Lecture 8 — 2018-02-08
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 Applicative
s 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…)