Expressing monoidal category in Haskell, part 2
In the previous post, we saw two monoidal categories in Hask. This time, we’ll see some monoidal categories in Hask^Hask. Hask^Hask is a category of functors in Haskell. Objects are functors and morphisms are natural transformations in this category.
When we saw monoidal categories in Hask, we used standard Functor and Bifunctor to express functors and bifunctors in it. This time, we’ll define Functor2 and Bifunctor2 by ourselves to express functors and bifunctors in Hask^Hask.
First, we’ll define ~> to express morpihsms in Hask^Hask. In Hask, -> expresses a morphism which is a function. In Hask^Hask, morphisms are natural transformations and are expressed as polymorphic functions f a -> g a for all a. We’ll write it as f ~> g to make it look more like a function.
{-# LANGUAGE AllowAmbiguousTypes,
QuantifiedConstraints,
TypeFamilies,
UndecidableInstances
#-}
import Control.Applicative
import Data.Functor.Compose
import Data.Functor.Day
import Data.Functor.Identity
import Data.Functor.Product
import Data.Kind
import Data.Proxy
type FunctorType :: Type
type FunctorType = Type -> Type
type (~>) :: FunctorType -> FunctorType -> Type
type f ~> g = forall a. f a -> g a
Next, just like we defined FunctorType in the previous post, we’ll define Functor2Type which is a function from a functor to a functor. It means that Functor2 maps a functor (FunctorType) to a functor (FunctorType). A functor is an object in Hask^Hask, so Functor2Type maps an object to an object in Hask^Hask.
type Functor2Type :: Type
type Functor2Type = FunctorType -> FunctorType
Actual functors need to be an instance of this Functor2 type class. Here, fmap2 maps a natural transformation ((~>)) to a natural transformation ((~>)). Since natural transformations are morphisms in Hask^Hask, you can say that fmap2 maps a morphism to a morphism in Hask^Hask.
type Functor2 :: Functor2Type -> Constraint
class Functor2 t where
fmap2 :: (Functor f, Functor g) => (f ~> g) -> (t f ~> t g)
We’ll do the same for bifunctors to have Bifunctor2Type and Bifunctor type class.
type Bifunctor2Type :: Type
type Bifunctor2Type = FunctorType -> FunctorType -> FunctorType
type Bifunctor2 :: Bifunctor2Type -> Constraint
class (forall f. (Functor f) => Functor2 (t f)) => Bifunctor2 t where
bimap2 ::
(Functor f, Functor g, Functor h, Functor i) =>
(f ~> h) ->
(g ~> i) ->
(t f g ~> t h i)
These types and type classes are functors (FunctorType and Functor) and bifunctors (BifunctorType and Bifunctor) lifted to Hask^Hask. When you compare them with Functor and Bifunctor, you can find that they look very similar except that Functor2 and Bifunctor2 use ~> instead of ->. This is so because morphisms in Hask are functions (->) while morphisms in Hask^Hask are natural transformations (~>).
Now let’s define type classes for monoidal categories and monoidal objects just like we did in the previous post.
As we saw there, we need a category, bifunctor and its unit to define a monoidal category. This time, a category is Hask^Hask. A bifunctor is Bifunctor2 that satisfies some conditions.
When f, g and h are objects in Hask^Hask (so they’re functors in Hask), we can express these conditions as
f `bif2` (g `bif2` h) ≅ (f `bif2` g) `f2` hf `bif2` u ≅ fu `bif2` f ≅ f
where bif2 is a bifunctor, and u is its unit. For example, when you use Compose as a bifunctor and Identity as its unit, they mean these conditions.
Compose f (Compose g h) ≅ Compose (Compose f g) hCompose f Identity ≅ fCompose Identity f ≅ f
Let’s express them as FunctorMonoidalCategory type class. We’ll express an ismorphism as two bidirectional natural transformations (morphisms) such as assoc and assocInv where assoc . assocInv = assocInv . assoc = id.
type FunctorMonoidalCategory :: Bifunctor2Type -> Constraint
class (Bifunctor2 t, Functor (FunctorUnit t)) => FunctorMonoidalCategory t where
type FunctorUnit t :: FunctorType
assoc :: (Functor f, Functor g, Functor h) => t f (t g h) ~> t (t f g) h
assocInv :: (Functor f, Functor g, Functor h) => t (t f g) h ~> t f (t g h)
left :: (Functor f) => t (FunctorUnit t) f ~> f
leftInv :: (Functor f) => f ~> t (FunctorUnit t) f
right :: (Functor f) => t f (FunctorUnit t) ~> f
rightInv :: (Functor f) => f ~> t f (FunctorUnit t)
In this monoidal category, we can express a monoid object as an object that has these two operations.
mu :: `f `bif2` f ~> feta :: u ~> f
We’ll express these two in FunctorMonoidObject type class.
type FunctorMonoidObject :: Bifunctor2Type -> FunctorType -> Constraint
class (FunctorMonoidalCategory t, Functor f) => FunctorMonoidObject t f where
mu :: t f f ~> f
eta :: FunctorUnit t ~> f
This construct has the same structure as MonoidalCategory and MonoidObject in the previous post, but they’re lifted to Hask^Hask.
Let’s take an example. In this example, we’ll use Compose as a bifunctor, and Identity as its unit. First, let’s make Compose an instance of Functor2 and Bifunctor2.
instance (Functor f) => Functor2 (Compose f) where
fmap2 ::
(Functor g, Functor h) =>
(g ~> h) ->
(Compose f g ~> Compose f h)
fmap2 gh (Compose fga) = Compose (fmap gh fga)
instance
(forall f. (Functor f) => Functor2 (Compose f)) =>
Bifunctor2 Compose
where
bimap2 ::
(Functor f, Functor g, Functor h, Functor i) =>
(f ~> h) ->
(g ~> i) ->
(Compose f g ~> Compose h i)
bimap2 fh gi (Compose fga) =
let fia = fmap gi fga
in Compose (fh fia)
With them, you can make it an instance of FunctorMonoidalCategory.
instance FunctorMonoidalCategory Compose where
type FunctorUnit Compose = Identity
assoc ::
(Functor f, Functor g, Functor h) =>
Compose f (Compose g h) ~> Compose (Compose f g) h
assoc (Compose fcgha) =
let fgha = fmap (\(Compose gha) -> gha) fcgha
in Compose (Compose fgha)
assocInv ::
(Functor f, Functor g, Functor h) =>
Compose (Compose f g) h ~> Compose f (Compose g h)
assocInv (Compose (Compose fgh)) = Compose (fmap Compose fgh)
left :: (Functor f) => Compose Identity f ~> f
left (Compose (Identity f)) = f
leftInv :: f ~> Compose Identity f
leftInv f = Compose (Identity f)
right :: (Functor f) => Compose f Identity ~> f
right (Compose fia) = fmap (\(Identity a) -> a) fia
rightInv :: (Functor f) => f ~> Compose f Identity
rightInv fa = Compose (fmap Identity fa)
You can make, for example, Maybe an instance of FunctorMonoidObject with FunctorMonoidalCategory. This means that Maybe is a monoid object in the monoidal category that consists of Hask^Hask, Compose and Identity.
instance FunctorMonoidObject Compose Maybe where
mu :: Compose Maybe Maybe ~> Maybe
mu (Compose (Just (Just a))) = Just a
mu (Compose _) = Nothing
eta :: Identity ~> Maybe
eta (Identity a) = Just a
You can make a list ([]) an instance as well.
instance FunctorMonoidObject Compose [] where
mu :: Compose [] [] ~> []
mu (Compose a) = concat a
eta :: Identity ~> []
eta (Identity a) = [a]
As we saw in Monoid of functors, this monoid object is identical to a monad. Actually, we can derive an instance of Applicative and Monad from FunctorMonoidObject.
instance
{-# OVERLAPPABLE #-}
( Functor f,
FunctorMonoidObject Compose f
) =>
Applicative f
where
(<*>) :: f (a -> b) -> (f a -> f b)
(<*>) fab fa =
mu $
Compose $
fmap
( \ab ->
mu $ Compose $ fmap (pure . ab) fa
)
fab
pure :: a -> f a
pure = eta @Compose . Identity
instance
{-# OVERLAPPABLE #-}
( Functor f,
FunctorMonoidObject Compose f
) =>
Monad f
where
(>>=) :: f a -> (a -> f b) -> f b
(>>=) fa afb = mu $ Compose $ fmap afb fa
Let’s take another example. This time, we’ll use Day as a bifunctor and use Identity as its unit again.
First, make Day an instance of Functor2 and Bifunctor2.
instance (Functor f) => Functor2 (Day f) where
fmap2 ::
(Functor g, Functor h) =>
(g ~> h) ->
(Day f g ~> Day f h)
fmap2 gh (Day f g bca) = Day f (gh g) bca
instance (forall f. (Functor f) => Functor2 (Day f)) => Bifunctor2 Day where
bimap2 ::
(Functor f, Functor g, Functor h, Functor i) =>
(f ~> h) ->
(g ~> i) ->
(Day f g ~> Day h i)
bimap2 fh gi (Day f g bca) = Day (fh f) (gi g) bca
Then, you can make it an instance of FunctorMonoidalCategory.
instance FunctorMonoidalCategory Day where
type FunctorUnit Day = Identity
assoc ::
(Functor f, Functor g, Functor h) =>
Day f (Day g h) ~> Day (Day f g) h
assoc (Day fb (Day gc hd cde) bea) =
Day (Day fb gc (,)) hd (\(b, c) d -> bea b (cde c d))
assocInv ::
(Functor f, Functor g, Functor h) =>
Day (Day f g) h ~> Day f (Day g h)
assocInv (Day (Day fb gc bcd) he dea) =
Day fb (Day gc he (,)) (\b (c, e) -> dea (bcd b c) e)
left :: (Functor f) => Day Identity f ~> f
left (Day (Identity b) fc bca) = fmap (bca b) fc
leftInv :: (Functor f) => f ~> Day Identity f
leftInv fa = Day (Identity ()) fa (flip const)
right :: (Functor f) => Day f Identity ~> f
right (Day fb (Identity c) bca) = fmap (flip bca c) fb
rightInv :: (Functor f) => f ~> Day f Identity
rightInv fa = Day fa (Identity ()) const
It might not look that left . leftInv = leftInv . left = id and right . rightInv = rightInv . right = id hold, but you can see Day Identity f and f are isomorphic in this post.
Now we have a category (Hask^Hask), a bifunctor (Day) and its unit (Identity), let’s make Maybe and [] a monoid object in it.
instance FunctorMonoidObject Day Maybe where
mu :: Day Maybe Maybe ~> Maybe
mu (Day (Just b) (Just c) bca) = Just (bca b c)
mu _ = Nothing
eta :: Identity ~> Maybe
eta (Identity a) = Just a
instance FunctorMonoidObject Day [] where
mu :: Day [] [] ~> []
mu (Day bs cs bca) = [bca b c | b <- bs, c <- cs]
eta :: Identity ~> []
eta (Identity a) = [a]
This time, this monoid object is identical to Applicative. You can implement Applicative from this FunctorMonoidObject.
instance
{-# OVERLAPPABLE #-}
( Functor f,
FunctorMonoidObject Day f
) =>
Applicative f
where
(<*>) :: f (a -> b) -> (f a -> f b)
(<*>) fab fa = mu $ Day fab fa (\ab a -> ab a)
pure :: a -> f a
pure = eta @Day . Identity
It’s interesting that we can derive Applicative both from the first monoidal category (Hask^Hask, Compose and Identity), and the second monoidal category (Hask^Hask, Day and Identity).
Finally, we’ll pick one more example. This time, we use Product as a bifunctor and use Proxy as its unit.
Again, we’ll first make Product an instance of Functor2 and Bifunctor2.
instance (Functor f) => Functor2 (Product f) where
fmap2 ::
(Functor g, Functor h) =>
(g ~> h) ->
(Product f g ~> Product f h)
fmap2 gh (Pair fa ga) = Pair fa (gh ga)
instance
(forall f. (Functor f) => Functor2 (Product f)) =>
Bifunctor2 Product
where
bimap2 ::
(Functor f, Functor g, Functor h, Functor i) =>
(f ~> h) ->
(g ~> i) ->
(Product f g ~> Product h i)
bimap2 fh gi (Pair fa ga) = Pair (fh fa) (gi ga)
Then, make it an instance of FunctorMonoidalCategory with Proxy as its unit.
instance FunctorMonoidalCategory Product where
type FunctorUnit Product = Proxy
assoc ::
(Functor f, Functor g, Functor h) =>
Product f (Product g h)
~> Product
(Product f g)
h
assoc (Pair fa (Pair ga ha)) = Pair (Pair fa ga) ha
assocInv ::
(Functor f, Functor g, Functor h) =>
Product (Product f g) h ~> Product f (Product g h)
assocInv (Pair (Pair fa ga) ha) = (Pair fa (Pair ga ha))
left :: (Functor f) => Product Proxy f ~> f
left (Pair Proxy fa) = fa
leftInv :: (Functor f) => f ~> Product Proxy f
leftInv fa = Pair Proxy fa
right :: (Functor f) => Product f Proxy ~> f
right (Pair fa Proxy) = fa
rightInv :: (Functor f) => f ~> Product f Proxy
rightInv fa = Pair fa Proxy
Again, Maybe and [] can be a monoid object in this monoidal category, too.
instance FunctorMonoidObject Product Maybe where
mu :: Product Maybe Maybe ~> Maybe
mu (Pair (Just a) _) = Just a
mu (Pair _ (Just a)) = Just a
mu _ = Nothing
eta :: Proxy ~> Maybe
eta _ = Nothing
instance FunctorMonoidObject Product [] where
mu :: Product [] [] ~> []
mu (Pair as bs) = as <> bs
eta :: Proxy ~> []
eta _ = []
As you might have expected, this monoid object is identical to Alternative (but the functor has to be Applicative because Alternative has (theoretically unnecessary) Applicative constraint), and you can derive Alternative from this monoid object.
instance
{-# OVERLAPPABLE #-}
( Functor f,
Applicative f,
FunctorMonoidObject Product f
) =>
Alternative f
where
(<|>) :: f a -> f a -> f a
(<|>) fa1 fa2 = mu (Pair fa1 fa2)
empty :: f a
empty = eta @Product Proxy