Learn You a Haskell for Great Good! - Miran Lipovaca [103]
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
This simple three-line class definition tells us a lot! The first line starts the definition of the Applicative class, and it also introduces a class constraint. The constraint says that if we want to make a type constructor part of the Applicative type class, it must be in Functor first. That’s why if we know that a type constructor is part of the Applicative type class, it’s also in Functor, so we can use fmap on it.
The first method it defines is called pure. Its type declaration is pure :: a -> f a. f plays the role of our applicative functor instance here. Because Haskell has a very good type system, and because all a function can do is take some parameters and return some value, we can tell a lot from a type declaration, and this is no exception.
pure should take a value of any type and return an applicative value with that value inside it. “Inside it” refers to our box analogy again, even though we’ve seen that it doesn’t always stand up to scrutiny. But the a -> f a type declaration is still pretty descriptive. We take a value and we wrap it in an applicative value that has that value as the result inside it. A better way of thinking about pure would be to say that it takes a value and puts it in some sort of default (or pure) context—a minimal context that still yields that value.
The <*> function is really interesting. It has this type declaration:
f (a -> b) -> f a -> f b
Does this remind you of anything? It’s like fmap :: (a -> b) -> f a-> f b. You can think of the <*> function as sort of a beefed-up fmap. Whereas fmap takes a function and a functor value and applies the function inside the functor value, <*> takes a functor value that has a function in it and another functor, and extracts that function from the first functor and then maps it over the second one.
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
Again, from the class definition, we see that the f that plays the role of the applicative functor should take one concrete type as a parameter, so we write instance Applicative Maybe where instead of instance Applicative (Maybe a) where.
Next, we have pure. Remember that it’s supposed to take something and wrap it in an applicative value. We wrote pure = Just, because value constructors like Just are normal functions. We could have also written pure x = Just x.
Finally, we have the definition for <*>. We can’t extract a function out of a Nothing, because it has no function inside it. So we say that if we try to extract a function from a Nothing, the result is a Nothing.
In the class definition for Applicative, there’s a Functor class constraint, which means that we can assume that both of the <*> function’s parameters are functor values. If the first parameter is not a Nothing, but a Just with some function inside it, we say that we then want to map that function over the second parameter. This also takes care of the case where the second parameter is Nothing, because doing fmap with any function over a Nothing will re turn a Nothing. So for Maybe, <*> extracts the function from the left value if it’s a Just and maps it over the right value. If any of the parameters is Nothing, Nothing is the result.
Now let’s give this a whirl:
ghci> Just (+3) <*> Just 9
Just 12
ghci> pure (+3) <*> Just 10
Just 13
ghci> pure (+3) <*> Just 9
Just 12
ghci> Just (++"hahah") <*> Nothing
Nothing
ghci> Nothing <*> Just "woot"
Nothing
You see how doing pure (+3) and Just (+3) is the same in this case. Use pure if you’re dealing with Maybe values in an applicative context (using them with <*>); otherwise, stick to Just.
The first four input lines demonstrate how the function is extracted and