Road to Haskeller #14 - Applicative

Last Edited: 6/27/2024

The blog post introduces an important typeclass, Applicative, in Haskell.

Haskell & Applicative

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 b

The 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 something

We 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 <*> getLine

Exercises

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