Skip to the content.

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

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.

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.

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