The blog post introduces an important typeclass, Applicative, in Haskell.
Applicative Functors
Applicative functors are an upgraded version of functors. If you are not confident about functors,
check out the last article, Road to Haskeller #13 - Functors. Let's think about using fmap on a function
that requires two parameters, like fmap (*) (Just 3). What will happen? This will create a partial
function in Just, like Just (* 3), which can be passed to another fmap to perform further operations.
ghci> :t fmap (*) (Just 3)
fmap (*) (Just 3) :: Num a => Maybe (a -> a)
ghci> let b = fmap (*) (Just 3)
ghci> fmap (\f -> f 9) b
Just 27
However, if we do fmap (\f -> f (Just 9)) b, it will not work as fmap only applies functions that have
the same type as inside of a functor. To allow functions over another functor to be applied to a functor,
the Applicative typeclass is defined in the Control.Applicative module. The class is defined like the following.
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f bThe first part (Functor f) => Applicative means f needs to be a functor first for it to become Applicative.
Hence, all the type constructors in the Applicative typeclass are also functors, and we can use fmap on them.
pure is straightforward, as it creates an applicative functor with an input value inside it.
<*> is an interesting function that takes a functor with a function inside of it and applies that function over a functor.
It means with <*>, we can do something like the following.
ghci> (<*>) (Just (*3)) (Just 9)
Just 27
ghci> Just (*3) <*> Just 9
Just 27
Let's take a look at how Maybe is a valid instance of Applicative.
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just f) <*> something = fmap f somethingWe can see that pure is defined with partial function application to wrap a value with Just, and <*> avoids operation
when Nothing is provided while <*> extracts a function from Just and passes it to fmap. Using these functions,
we can achieve interesting things like the following.
ghci> pure (+) <*> Just 3 <*> Just 5
Just 8
ghci> Just (*) <*> Just 3 <*> Just 5
Just 15
By using this setup from Applicative, we can now use functions with multiple parameters on values in applicative functors
to generate partial functions or an output value. Alternatively to pure and using the default value constructor and using <*>,
we can use <$> to perform fmap on a function and a first functor to create a functor for the subsequent operations.
ghci> (+) <$> Just 3 <*> Just 5
Just 8
The above is doing fmap (+) (Just 3) to create Just (+ 3) and using <*> to perform operations inside of Maybe.
Lists
A list is a valid instance of the Applicative typeclass as well, which is implemented as follows.
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]We can see from the above that list comprehension is utilized in defining <*>. If you are not confident
about list comprehensions, check out Road to Haskeller #3 - Lists & Tuples. Similarly to Maybe, we can do
something like the following.
ghci> [(*0),(+100),(^2)] <*> [1,2,3]
[0,0,0,101,102,103,1,4,9]
As we can see from the above, <*> goes over the left list and performs the operation on each element
on the right list. We can understand the order of operations better by looking at the example below.
[(+),(*)] <*> [1,2] <*> [3,4]
--- Firstly, [(+1), (+2), (*1), (*2)] <*> [3, 4]
--- Then, [(3+1), (4+1), (3+2), (4+2), (3*1), (4*1), (3*2), (4*2)]
--- Result: [4, 5, 5, 6, 3, 4, 6, 8]We can also use <$> to do something like the following.
filter (>50) $ (*) <$> [2,5,10] <*> [8,10,11]
--- The above translates to:
--- filter (>50) $ [(*2), (*5), (*10)] <*> [8,10,11]
--- filter (>50) [(8*2), (10*2), (11*2), (8*5), (10*5), (11*5), (8*10), (10*10), (11*10)]
--- filter (>50) [16, 20, 22, 40, 50, 55, 80, 100, 110]
--- Result: [55, 80, 100, 110]IO
IO is also a valid instance of Applicative, and the following is how the instance is implemented.
instance Applicative IO where
pure = return
a <*> b = do
f <- a
x <- b
return (f x)The pure function is simply return as return generates an IO action that does not do anything.
The <*> function makes use of <- syntax to extract the output of the IO actions to perform inner operations.
Then, it wraps the result with return to get back to the IO action. This means the following refactoring can be performed.
--- From
myAction :: IO String
myAction = do
a <- getLine
b <- getLine
return $ a ++ b
--- To
myAction = (++) <$> getLine <*> getLineExercises
This is an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.
Resources
- NA. Functors, Applicative Functors and Monoids. Learn You a Haskell for Great Good.