The Applicative type class is defined as follows:
class Functor f => Applicative (f :: * -> *) where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
(*>) :: f a -> f b -> f b
(<*) :: f a -> f b -> f a
{-# MINIMAL pure, (<*>) #-}
The minimal definition of an Applicative instance requires at least pure and <*> to be defined. The definition also implies that we can define an instance of an Applicative for f only if f is also an instance of a Functor.
The pure function takes a value and creates a data type. For example, in the context of List, Maybe, and Either, the pure function will fetch the following values:
pure 10 :: [Int] = [10] -- in the context of List
pure 10 :: Maybe Int = Just 10 -- in the context of Maybe
pure 10 :: Either String Int = Right 10 -- in the context of Either
You can try the preceding code in the GHCi console for the project by running stack ghci in the project directory and trying out the preceding expressions.
Now we will look at the core of an Applicative, that is, the function <*>. As explained earlier, this function has the form as described:
<*> :: Applicative f => f (a -> b) -> f a -> f b
Remember the definition of a Functor and fmap:
fmap :: (a -> b) -> f a -> f b
If we take a function (a -> b -> c) and call fmap on a Functor instance, we will get the following code:
fmap :: (a -> b -> c) -> f a -> f (b -> c)
This is interesting because the application of fmap resulted in f (b -> c). Now, we can take f (b->c) and apply it to f b using <*> and get f c. Thus, we can use a function such as (*) :: a -> a -> a and use it in the conjunction of <$> and <*> to apply more complex things such as multiplication on a couple of Maybes:
(*) <$> Just 10 <*> Just 2 -- Will produce Just 20
(*) <$> Nothing <*> Just 2 -- Will produce Nothing
This way, one can see that the Applicative extends Functor by adding more expressiveness to it.
An Applicative does much more than just applying a function with multiple arguments to a data type. In the Applicative, we will encapsulate the function in the data type f (a -> b) and apply it to the data type with f a. This way, it is possible to carry more information in the structure f and apply it during the evaluation and application of the encapsulated function.
For example, one can consider f a as an operation carried out in parallel; f (a -> b) denotes that it needs to wait for the value to be produced by f a and then apply the function. Furthermore, we can create an Applicative type that represents a thread pool and schedules f a on each one of them, retaining the composing power of functions.