An Applicative
is a value encapsulated within some sort of “effect” context. In the case of Maybe
this context is the possibility of “failure” (None
). Either e
is very similar, in that it represents a value whose calculation may have failed, but with an informative error e
. Still almost every encapsulated effect has a valid Applicative
instance (not all of these are available in the Daml sdk, but all are supported in Daml):
-
Update
encapsulates a ledger interaction effect
-
[a]
encapsulates the idea of multi-return effect
-
Reader
encapsulates a dependency injection effect
-
State
encapsulates a mutable context effect
-
Random
encapsulates a context maintaining a PRNG
-
Identity
a pure context (the non-context context)
All of these are Functors, which means they support fmap
/<$>
, but fmap
really is the bare minimum for an encapsulating context, all it allows you to do is to compose encapsulated values with pure functions:
fmap : Functor f => (a -> b) -> (f a -> f b)
So we take a function a -> b
and turn it into a function that works with encapsulated values f a -> f b
.
But there are many more things we want to do with functions, and the key one supporting all of functional programming is function application (ie. ($)
).
($) : (a -> b) -> a -> b
But now consider what happens when instead of a function (a -> b)
you have a “function in a context”?We want to be able to apply it, so compare the following:
($) : (a -> b) -> a -> b
(<*>) : f (a -> b) -> f a -> f b
So another way to think of Applicative
is as a standardised API for encapsulated contexts to provide function application. For a concrete example consider the trying to add (+)
two integers that might have failed with an error (Either e Int
):
So what we want is
addE (Right 2) (Right 3) === Right 5
With pure values:
(+) 2 3 ===> 5
With functor we can do:
(+) <$> Right 2) ===> Right (2 +)
But now you have an Either e Int
, and if all we have is fmap
then as this is a function we can’t apply this to the second argument. We need the ability to apply the function currently encapsulated within the function. Ie. we need Applicative
:
(+) <$> Right 2 <*> Right 3 ===> Right 5
The final part of Applicative
is allowing you to apply this function to a pure value, so something along the lines of
addE (Right 2) 3 === Right 5
ie we need to be able to handle something along the lines of: Applicative f => (a -> b -> c) -> f a -> b -> f c
The easiest way to do this is to provide a convenient way to turn b
into f b
, and the way we do this is by requiring an Applicative
instance to provide a definition of pure : a -> f a
. So with these three tools (fmap
, (<*>)
, and pure
) we can build apply most of the standard FP techniques we are used to using on pure values now with encapsulated effectful values.
Of course, because these contexts are more complex and have more structure than a pure values there are often other things you will want to do that interact with that structure in someway, so Applicative
isn’t the be all and end all of API patterns available. The obvious examples are Action
, Foldable
, and Traversable
—but that is a discussion for another post.
I hope this helps provide some context.