Preliminaries
Applicative functors are pervasive in functional programming. Lists, options, effects, and a myriad others.
The definition of an applicative functor (in Haskell) is worth reviewing.
Why are pure and (<*>) bundled together like this? Surely we could define a hierarchy, as with Semigroup and Monoid?
The difference here is that we need pure, in order to define meaningful laws for (<*>).
The applicative laws are as follows:
-- Identity law
pure id <*> v = v
-- Homomorphism law
pure f <*> pure x = pure (f x)
-- Interchange law
u <*> pure y = pure ($ y) <*> u
-- Composition law
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)Biapplicative functors
With that in mind, let’s take a look at “biapplicative” functors. Not so different!
class Bifunctor p => Biapplicative p where
bipure :: a -> b -> p a b
(<<*>>) :: p (a -> b) (c -> d) -> p a c -> p b dBiapplicative functors are a generalization of applicative functors that support two type parameters, instead of one. Tuples are a familiar example of a type supporting a definition of Biapplicative.
What might be surprising is that it is not possible to define an instance of Biapplicative for Either.
instance Biapplicative Either where
-- We could try this with `Right` instead;
-- Since `Either` is symmetric, there is no difference.
bipure = Left . const
(<<*>>) (Left f) (Left x) = Left (f x)
(<<*>>) (Right g) (Right y) = Right (g y)
-- There is no way to define these cases
(<<*>>) (Left f) (Right y) =
(<<*>>) (Right g) (Left x) =We can, however, define (<<*>>) on Maybe (Either a b):
(<<*>>) (Just (Left f)) (Just (Left x)) = Just (Left (f x))
(<<*>>) (Just (Left _)) _ = Nothing
(<<*>>) (Just (Right g)) (Just (Right y)) = Just (Right (g y))
(<<*>>) (Just (Right _)) _ = Nothing
(<<*>>) Nothing _ = NothingWhat should we choose for bipure? All of the following seem reasonable:
At this point, it’s worth looking into the “Biapplicative laws”, which will tell us if we can define an instance Biapplicative Maybe (Either a b), and perhaps resolve the choice of bipure. The biapplicative laws are fairly intuitive - they are almost the same as the applicative laws.
-- Identity law
bipure id id <<*>> v = v
-- Homomorphism law
bipure f g <<*>> bipure x y = bipure (f x) (g y)
-- Interchange law
u <<*>> bipure x y = bipure ($ x) ($ y) <<*>> u
-- Composition law
bipure (.) (.) <<*>> u <<*>> v <<*>> w = u <<*>> (v <<*>> w)At this point we can clearly see that no choice of bipure will satisfy the identity law or composition laws for Maybe (Either a b).
Generalizing Biapplicative Functors
The definition of Applicative includes pure so that we can state the applicative laws. For the same reason, the definition of Biapplicative includes bipure. But does it have to? What if we were to rethink Biapplicative, with an alternative to bipure?
class Bifunctor p => Biapplicative p where
pureL :: a -> p a b
pureR :: b -> p a b
(<<*>>) :: p (a -> b) (c -> d) -> p a c -> p b dThis definition is much nicer to work with; The instance for Maybe (Either a b) is much as it was before:
instance Biapplicative Maybe (Either a b) where
pureL = Just . Left
pureR = Just . Right
(<<*>>) (Just (Left f)) (Just (Left x)) = Just (Left (f x))
(<<*>>) (Just (Left _)) _ = Nothing
(<<*>>) (Just (Right g)) (Just (Right y)) = Just (Right (g y))
(<<*>>) (Just (Right _)) _ = Nothing
(<<*>>) Nothing _ = NothingWhat about the laws for this definition? Since we have a left and right version of pure now, there are a few more laws:
-- Identity laws
pureL id <<*>> v = v
pureR id <<*>> v = v
-- Homomorphism laws
pureL f <<*>> pureL x = pureL (f x)
pureR g <<*>> pureR y = pureR (g y)
-- Interchange laws
u <<*>> pureL x = pureL ($ x) <<*>> u
u <<*>> pureR y = pureR ($ y) <<*>> u
-- Composition laws
pureL (.) <<*>> u <<*>> v <<*>> w = u <<*>> (v <<*>> w)
pureR (.) <<*>> u <<*>> v <<*>> w = u <<*>> (v <<*>> w)Unfortunately our instance still won’t satisfy the identity laws. What we really need is two versions of (<<*>>), in the same vein as how Bifunctor has two versions of Functor’s <$>.
class Bifunctor p => Biapplicative p where
pureL :: a -> p a b
pureR :: b -> p a b
applyL :: p (a -> b) (c -> c) -> p a c -> p b c
applyL = (<<*>>)
applyR :: p (a -> a) (b -> c) -> p a b -> p a c
applyR = (<<*>>)
(<<*>>) :: p (a -> b) (c -> d) -> p a c -> p b d
(<<*>>) p = (first (const id) p `applyR`) . (second (const id) p `applyL`)Now we can write some more laws:
-- Identity laws
pureL id `applyL` v = v
pureR id `applyR` v = v
-- Homomorphism laws
pureL f `applyL` pureL x = pureL (f x)
pureR g `applyL` pureR y = pureR (g y)
pureL f `applyR` pureL x = pureL (f x)
pureR g `applyR` pureR y = pureR (g y)
-- Interchange laws
u <<*>> pureL x = pureL ($ x) <<*>> u
u <<*>> pureR y = pureR ($ y) <<*>> u
-- Composition laws
pureL (.) <<*>> u <<*>> v <<*>> w = u <<*>> (v <<*>> w)
pureR (.) <<*>> u <<*>> v <<*>> w = u <<*>> (v <<*>> w)These laws are effectively the applicative functor laws but mirrored for each “side” of our biapplicative.
Finally, we can write a lawful instance!
instance Biapplicative Maybe (Either a b) where
pureL = Just . Left
pureR = Just . Right
applyL (Just (Left f)) (Just (Left x)) = Just (Left (f x))
applyL (Just (Left _)) (Just (Right y)) = pureR y
applyL (Just (Left _)) Nothing = Nothing
applyL (Just (Right g)) (Just (Right y)) = Just (Right (g y))
applyL (Just (Right _)) _ = Nothing
applyL Nothing _ = Nothing
applyR (Just (Right g)) (Just (Right y)) = Just (Right (g y))
applyR (Just (Right _)) (Just (Left x)) = pureL x
applyR (Just (Right _)) Nothing = Nothing
applyR (Just (Left f)) (Just (Left x)) = Just (Left (f x))
applyR (Just (Left _)) _ = Nothing
applyR Nothing _ = NothingThere’s still a problem. Because we’ve replaced bipure with pureL and pureR, we can no longer define an instance of Biapplicative (,).
I think that this is sufficiently motivating to split our Biapplicative definition into three parts.
class Bifunctor p => Biapplicative p where
applyL :: p (a -> b) (c -> c) -> p a c -> p b c
applyL = (<<*>>)
applyR :: p (a -> a) (b -> c) -> p a b -> p a c
applyR = (<<*>>)
(<<*>>) :: p (a -> b) (c -> d) -> p a c -> p b d
(<<*>>) p = (first (const id) p `applyR`) . (second (const id) p `applyL`)
class Biapplicative p => Biapplicative1 p where
pureL :: a -> p a b
pureR :: b -> p a b
class Biapplicative p => Biapplicative2 p where
bipure :: a -> b -> p a bThe laws for Biapplicative1 and Biapplicative2 are as we saw above, using pureL and pureR to describe the Biapplicative1 laws, and bipure to describe the Biapplicative2 laws.
Indeed, it is possible for a biapplicative functor to have instances of both Biapplicative1 and Biapplicative2.
One such example is given below:
data EitherBothNothing a b = B a b | L a | R b | N
instance Bifunctor EitherBothNothing where
bimap f g = \case
B x y -> B (f x) (g y)
L x -> L (f x)
R y -> R (g y)
N -> N
instance Biapplicative EitherBothNothing where
applyL (B f g) (B x y) = B (f x) (g y)
applyL (B f _) (L x) = L (f x)
applyL (B _ g) (R y) = R (g y)
applyL (L f) (B x y) = B (f x) y
applyL (L f) (L x) = L (f x)
applyL (L _) (R y) = R y
applyL (R g) (B _ y) = R (g y)
applyL (R _) (L x) = N
applyL (R g) (R y) = R (g y)
applyL N _ = N
applyL _ N = N
applyR (B f g) (B x y) = B (f x) (g y)
applyR (B f _) (L x) = L (f x)
applyR (B _ g) (R y) = R (g y)
applyR (L f) (B x _) = L (f x)
applyR (L f) (L x) = L (f x)
applyR (L _) (R _) = N
applyR (R g) (B x y) = B x (g y)
applyR (R _) (L x) = L x
applyR (R g) (R y) = R (g y)
applyR N _ = N
applyR _ N = N
(<<*>>) (B f g) (B x y) = B (f x) (g y)
(<<*>>) (B f _) (L x) = L (f x)
(<<*>>) (B _ g) (R y) = R (g y)
(<<*>>) (L f) (B x _) = L (f x)
(<<*>>) (L f) (L x) = L (f x)
(<<*>>) (L _) (R _) = N
(<<*>>) (R g) (B x y) = R (g y)
(<<*>>) (R _) (L _) = N
(<<*>>) (R g) (R y) = R (g y)
(<<*>>) N _ = N
(<<*>>) _ N = N
instance Biapplicative1 EitherBothNothing where
pureL = L
pureR = R
instance Biapplicative2 EitherBothNothing where
bipure = B