The applicative function <*> and Either

In the docs, I see that an Applicative defines an applicative <*> function:

class Functor f => Applicative f where
    <*> : f (a -> b) -> f a -> f b

I also see that Optional, Either, and lists (among others) are Applicatives.

instance Applicative Optional
instance Applicative (Either e)
instance Applicative ([])

I can build a simple example for Optional:

    let val1 : Optional Int = Some 3
    let val2 : Optional Int = None
    let convert1 : Optional (Int -> Decimal) = Some intToDecimal
    let convert2 : Optional (Int -> Decimal) = None

    debug $ convert1 <*> val1 -- Some 3.0
    debug $ convert1 <*> val2 -- None
    debug $ convert2 <*> val1 -- None
    debug $ convert2 <*> val2 -- None

But, I struggled to create a similar example for Either e.

Double-check me, but I think this is accurate:

The piece I was missing is that in the case of Either (really Either e) the applicative function is generally expected to map the Right value. In other words, for values of type Either a b, the applicative is expecting to be of signature Either a (b -> c).

Here is a simple example:

    let val : Either Text Int = Right 3
    let err : Either Text Int = Left "Cannot parse"
    let convert1 : Either Text (Int -> Decimal) = Right intToDecimal
    let convert2 : Either Text (Int -> Decimal) = Left "Error"

    debug $ convert1 <*> val -- Right 3.0
    debug $ convert1 <*> err -- Left "Cannot parse"
    debug $ convert2 <*> val -- Left "Error"
    debug $ convert2 <*> err -- Left "Error"

This StackOverflow helped me. Of course, now it makes perfect sense. :slight_smile:

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.

1 Like