「学习札记-Haskell」Chapter 11 Applicative Functions

「学习笔记-Haskell」Chapter 11 Applicative Functions

Chapter 11 Applicative Functions

Table of Contents

  • 1 Functors Redux
    • 1.1 I/O Actions As Functors
    • 1.2 Functions As Functors
  • 2 Functor Laws
    • 2.1 Law 1
    • 2.2 Law 2
    • 2.3 Breaking the Law
  • 3 Using Applicative Functors
    • 3.1 Say Hello to Applicative
    • 3.2 Maybe the Applicative Functor
    • 3.3 The Applicative Style
    • 3.4 Lists
    • 3.5 IO Is An Applicative Functor,Too
    • 3.6 Functions As Applicative
    • 3.7 Zip Lists
    • 3.8 Applicative Laws
      • 3.8.1 pure id <*> v = v
      • 3.8.2 pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
      • 3.8.3 pure f <*> pure x = pure (f x)
      • 3.8.4 u <*> pure y = pure ($ y) <*> u
  • 4 Useful Functions for Applicatives

1 Functors Redux

1.1 I/O Actions As Functors

Let’s see how IO is an instance of Functor. When we fmap a function over an I/O action, we want to get back an I/O action that does the same thing but has our function applied over its result value. Here’s the code:

instance Functor IO where
  fmap f action = do
    result <- action
    return (f result)

Check out this piece of code

main = do line <- getLine
          let line' = reverse line
          putStrLn $ "You said " ++ line' ++ " backwards!"
          putStrLn $ "Yes, you said " ++ line' ++ " backwards!"

We can rewrite it use fmap.

main = do line <- fmap reverse getLine
          putStrLn $ "You said " ++ line ++ " backwards!"
          putStrLn $ "Yes, you said " ++ line ++ " backwards!"

getLine is an I/O action that has a type of IO String, and mapping reverse over it gives us an I/O action that will go out into the real world and get a line and then apply reverse to its result.

We can also fmap multi functions to I/O action.

import Data.Char

main = do line <- fmap (reverse . map toUpper) getLine
          putStrLn $ "You said " ++ line ++ " backwards!"

1.2 Functions As Functors

Another instance of Functor that we’ve been dealing with all along is (->) r. If you understand the that r -> a can be write as (->) r a, you may understand what is (->) r.

How are functions functors? Let’s take a look at the implementation, which lies in Control.Monad.Instances.

instance Functor ((->) r) where
    fmap f g = (\x -> f (g x))

First, let’s think about fmap’s type:

fmap :: (a -> b) -> f a -> f b

Next, let’s mentally replace each f, which is the role that our functor instance plays, with (->) r.

fmap :: (a -> b) -> ((->) r a) -> ((->) r b)

Then, we change prefix to infix.

fmap :: (a -> b) -> (r -> a) -> (r -> b)

We see that it takes a function from a to b and a function from r to a and returns a function from r to b. Does this remind you of anything? Yes, function composition!

Here’s another way to write this instance.

instance Functor ((->) r) where
    fmap = (.)

This makes it clear that using fmap over functions is just function composition. Let's see several examples.

ghci>(*3) . (+100) $ 2
306
ghci>(*3) `fmap` (+100) $ 2
306
ghci>fmap (*3) (+100) 2
306

Note that you should import Control.Monad.Instances first.

Using fmap (*3) on (+100) will create another function that acts like (+100), but before producing a result, (*3) will be applied to that result.Think about the similarity between this process with functors on IO action.

2 Functor Laws

All functors are expected to exhibit certain kinds of properties and behav- iors. They should reliably behave as things that can be mapped over. This behavior is described in the functor laws. All instances of Functor should abide by these two laws.

2.1 Law 1

The first functor law states that if we map the id function over a functor value, the functor value that we get back should be the same as the original functor value.

ghci>id (Just 4)
Just 4
ghci>fmap id (Just 4)
Just 4

2.2 Law 2

The second law says that composing two functions and then mapping the resulting function over a functor should be the same as first mapping one function over the functor and then mapping the other one.

fmap (f . g) = fmap f . fmap g

2.3 Breaking the Law

Let’s take a look at a pathological example of a type constructor being an instance of the Functor type class but not really being a functor, because it doesn’t satisfy the laws.

data CMaybe a = CNothing | CJust Int a deriving (Show)

Let’s play with our new type

ghci>CNothing 
CNothing
ghci>CJust 1 "hiahia"
CJust 1 "hiahia"

Let’s make this an instance of Functor so that each time we use fmap, the func- tion is applied to the second field, whereas the first field is increased by 1

data CMaybe a = CNothing | CJust Int a deriving (Show)
instance Functor CMaybe where
  fmap f CNothing = CNothing
  fmap f (CJust counter x) = CJust (counter+1) (f x)
ghci>fmap (++"hahia") (CJust 0 "ho")
CJust 1 "hohahia"
ghci>fmap (++"xxx") $ fmap (++"hahia") (CJust 0 "ho")
CJust 2 "hohahiaxxx"

Does this obey the functor laws?We can find a counterexample easily.

ghci>id (CJust 0 "hia")
CJust 0 "hia"
ghci>fmap id (CJust 0 "hia")
CJust 1 "hia"

Since CMaybe fails at being a functor even though it pretends to be one, using it as a functor might lead to some faulty code.

The next time you make a type an instance of Functor, take a minute to make sure that it obeys the functor laws.

3 Using Applicative Functors

So far, we have focused on mapping functions that take only one parameter over functors. But what happens when we map a function that takes two parameters over a functor?

ghci>:t fmap (*) (Just 3)
fmap (*) (Just 3) :: Num a => Maybe (a -> a)

We see how by mapping “multiparameter” functions over functor val- ues, we get functor values that contain functions inside them. So now what can we do with them?

ghci>let a =  fmap (*) (Just 3)
ghci>fmap (\f -> f 4) a
Just 12

You can think of this process like.

a = fmap (*) (Just 3) 
-> a = (Just (*) 3)
-> a = (Just (3 *) )
fmap (\f -> f 4) a
= fmap (\f -> f 4) (Just (3 *) )
= (Just (\f -> f 4) (3 *) )
= (Just ((3 *) 4) )
= (Just (3 * 4) )

3.1 Say Hello to Applicative

It doesn’t provide a default implementation for either of them, so we need to define them both if we want something to be an applicative functor. The class is defined like so

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

3.2 Maybe the Applicative Functor

Let’s take a look at the Applicative instance implementation for Maybe.

instance Applicative Maybe where
    pure = Just
    Nothing <*> _ = Nothing
    (Just f) <*> something = fmap f something

Now let’s give some examples:

ghci>Just (3+) <*> Just 4
Just 7
ghci>pure (3+) <*> Just 4
Just 7

You see how doing pure (+3) and Just (+3) is the same in this case.

3.3 The Applicative Style

With the Applicative type class, we can chain the use of the <*> function, thus enabling us to seamlessly operate on several applicative values instead of just one.

ghci>pure (+) <*> Just 3 <*> Just 5
Just 8
ghci>pure (+) <*> Just 3 <*> Nothing
Nothing
ghci>pure (+) <*> Nothing <*> Just 5
Nothing

Instead of writing pure f <*> x <*> y <*> …, we can write fmap f x <*> y <*> …. This is why Control.Applicative exports a function called <$>, which is just fmap as an infix operator. Here’s how it’s defined:

(<$>) :: (Functor f) => (a -> b) -> f a -> f b
f <$> x = fmap f x

By using <$>, the applicative style really shines, because now if we want to apply a function f between three applicative values, we can write f <$> x <*> y <*> z. If the parameters were normal values rather than ap- plicative functors, we would write f x y z.

ghci>(++) <$> Just "sdf" <*> Just "ewrq"
Just "sdfewrq"

3.4 Lists

Lists (actually the list type constructor, []) are applicative functors.

instance Applicative [] where
  pure x = [x]
  fs <*> xs = [f x | f <- fs, x<-xs]

Let's look at some example to see what's the meaning of the definition above.

ghci>pure "Hia" :: [String]
["Hia"]
ghci>pure "Hia" :: Maybe String
Just "Hia"
ghci>[(*0),(+1),(^2)] <*> [1,2,3]
[0,0,0,2,3,4,1,4,9]

In the following example, we apply two function between two lists:

ghci>[(*),(+)] <*> [1,2] <*> [3,4]
[3,4,6,8,4,5,5,6]

Using the applicative style with lists is fun!

ghci>(++) <$> ["a","b","c"] <*> ["?","!","~"]
["a?","a!","a~","b?","b!","b~","c?","c!","c~"]

3.5 IO Is An Applicative Functor,Too

Another instance of Applicative that we’ve already encountered is IO.

instance Applicative IO where
  pure = return
  a <*> b = do
    f <- a
    x <- b
    return (f x)

Consider this:

myAction :: IO String
myAction = do
  a <- getLine
  b <- getLine
  return $ a ++ b

Another way of writing this is to use the applicative style.

myAction :: IO String
myAction = (++) <$> getLine <*> getLine

3.6 Functions As Applicative

Another instance of Applicative is (->) r, or functions.

instance Applicative ((->) r) where
  pure x = (\_ -> x)
  f <*> g = (\x -> f x ( g x))

Example:

ghci>(pure 3) "sdf"
3
ghci>(+) <$> (+3) <*> (*100) $ 4
407
ghci>(*) <$> (+3) <*> (*100) $ 4
2800

When we do (+) <$> (+3) <*> (*100), we’re making a function that will use + on the results of (3) and (*100) and return that. With () <$> (+3) <*> (*100) $ 5, (+3) and (*100) are first applied to 5, resulting in 8 and 500. Then + is called with 8 and 500, resulting in 508. The following example is similar.

ghci>(\x y z -> [x,y,z]) <$> (+3) <*> (*4) <*> (^2) $ 5
[8,20,25]

3.7 Zip Lists

An instance of Applicative that we haven’t encountered yet is ZipList, and it lives in Control.Applicative.

instance Applicative ZipList where
  pure x = ZipList (repeat x)
  ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs)

3.8 Applicative Laws

Like normal functors, applicative functors come with a few laws.

 pure id <*> v = v

 pure (.) <*> u <*> v <*> w = u <*> (v <*> w)

 pure f <*> pure x = pure (f x)

 u <*> pure y = pure ($ y) <*> u

4 Useful Functions for Applicatives

Control.Applicative defines a function that’s called liftA2

liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f <$> a <*> b

With ordinary functors, we can just map functions over one functor value. With applicative functors, we can apply a function between several functor values.

ghci>liftA2 (:) (Just 3) (Just [4])
Just [3,4]

Let’s try implementing a function that takes a list of applicative values and returns an applicative value that has a list as its result value. We’ll call it sequenceA.

sequnceA :: (Applicative f) => [f a] -> f [a]
sequnceA [] = pure []
sequnceA (x:xs) = (:) <$> x <*> sequnceA xs

Test it!

ghci>sequnceA [Just 1, Just 2, Just 3]
Just [1,2,3]
ghci>sequnceA [Just 1, Nothing, Just 3]
Nothing
ghci>sequnceA [(+1), (*2), (^3)] 3
[4,6,27]