Skip to the content.

Expressing monoidal category in Haskell, part 1

In Haskell, we have Monoid type class to express monoid. This can be seen as a monoid object in a monoidal category in category Hask where bifunctor is (,) and its unit is ().

In this post, we’ll see how we can express a monoidal category and a monoid object themselves in Haskell.

First, let’s define some kinds. We use FunctorType for a type constructor taking one type, and BifunctorType for a type constructor taking two types. We expect these types implement Functor and Bifunctor respectively.

{-# LANGUAGE AllowAmbiguousTypes,
             TypeFamilies,
             UndecidableInstances
#-}

import Data.Bifunctor
import Data.Kind
import Data.Void

type FunctorType :: Type
type FunctorType = Type -> Type

type BifunctorType :: Type
type BifunctorType = Type -> Type -> Type

To define a monoidal category, we need a category, a bifunctor, and its unit. We’ve already chosen Hask as a category. So all we need are a bifunctor and its unit.

This bifunctor has to satisfy some conditions. When you pick (,) as a bifunctor and () as its unit, we can express these conditions as follows.

In Haskell, we’ll express these isomorphisms as these functions

where assoc . assocInv = assocInv . assoc = id, left . leftInv = leftInv . left = id, right . rightInv = rightInv . right = id.

Let’s expand it to any bifunctor and make it a type class.

type MonoidalCategory :: BifunctorType -> Constraint
class (Bifunctor t) => MonoidalCategory t where
  type Unit t :: Type

  assoc :: t a (t a a) -> t (t a a) a
  assocInv :: t (t a a) a -> t a (t a a)

  left :: t (Unit t) a -> a
  leftInv :: a -> t (Unit t) a

  right :: t a (Unit t) -> a
  rightInv :: a -> t a (Unit t)

We can say that a bifunctor implements this type class satisfying the conditions is a monoidal category. Of course, (,) can be an instance of this type class.

instance MonoidalCategory (,) where
  type Unit (,) = ()

  assoc :: (a, (b, c)) -> ((a, b), c)
  assoc (a, (b, c)) = ((a, b), c)

  assocInv :: ((a, b), c) -> (a, (b, c))
  assocInv ((a, b), c) = (a, (b, c))

  left :: ((), a) -> a
  left ((), a) = a

  leftInv :: a -> ((), a)
  leftInv a = ((), a)

  right :: (a, ()) -> a
  right (a, ()) = a

  rightInv :: a -> (a, ())
  rightInv a = (a, ())

In the monoidal category of (,) and (), we can express monoid objects as objects that have these two operations.

We’ll have this MonoidObject type class by expanding this to any bifunctor. We use multi-param type classes instead of type families so that one type can be monoidal objects in different monoidal categories.

type MonoidObject :: BifunctorType -> Type -> Constraint
class (MonoidalCategory t) => MonoidObject t a where
  mu :: t a a -> a
  eta :: Unit t -> a

For example, we can define some monoidal objects in this monoidal category.

instance MonoidObject (,) Int where
  mu :: (Int, Int) -> Int
  mu (n, m) = n + m

  eta :: () -> Int
  eta () = 0

instance (MonoidObject (,) a) => MonoidObject (,) (Maybe a) where
  mu :: (Maybe a, Maybe a) -> Maybe a
  mu (Just a1, Just a2) = Just $ mu (a1, a2)
  mu (Just a1, _) = Just a1
  mu (_, Just a2) = Just a2
  mu _ = Nothing

  eta :: () -> Maybe a
  eta () = Nothing

instance MonoidObject (,) [a] where
  mu :: ([a], [a]) -> [a]
  mu (a1, a2) = a1 ++ a2

  eta :: () -> [a]
  eta () = []

instance MonoidObject (,) (a -> a) where
  mu :: (a -> a, a -> a) -> (a -> a)
  mu (f, g) = g . f

  eta :: () -> (a -> a)
  eta () = id

Now, let’s give another monoidal category a look. We use Hask as its category, and use Either and Void as its bifunctor and its unit.

instance MonoidalCategory Either where
  type Unit Either = Void

  assoc :: Either a (Either b c) -> Either (Either a b) c
  assoc (Left a) = Left (Left a)
  assoc (Right (Left b)) = Left (Right b)
  assoc (Right (Right c)) = Right c

  assocInv :: Either (Either a b) c -> Either a (Either b c)
  assocInv (Left (Left a)) = Left a
  assocInv (Left (Right b)) = Right (Left b)
  assocInv (Right c) = Right (Right c)

  left :: Either Void a -> a
  left (Left v) = absurd v
  left (Right a) = a

  leftInv :: a -> Either Void a
  leftInv = Right

  right :: Either a Void -> a
  right (Left a) = a
  right (Right v) = absurd v

  rightInv :: a -> Either a Void
  rightInv = Left

In this monoidal category, all types are monoid objects, and it’s not very fun. Still, this means that we can have monoidal categories other than (,) and () in Haskell.

instance MonoidObject Either a where
  mu :: Either a a -> a
  mu (Left a) = a
  mu (Right a) = a

  eta :: Void -> a
  eta = absurd

Getting back to the first monoidal category (Hask, (,) and ()), we can derive the standard Monoid (and Semigroup) from it.

instance {-# OVERLAPPABLE #-} (MonoidObject (,) a) => Semigroup a where
  (<>) :: a -> a -> a
  (<>) a1 a2 = mu (a1, a2)

instance {-# OVERLAPPABLE #-} (MonoidObject (,) a) => Monoid a where
  mempty :: a
  mempty = eta @(,) ()

You can see mappend 1 2 :: Int is evaluated to 3 through MonoidObject, for example. Note that it’ll not evaluated to 3 without MonoidObject because Int isn’t an instance of Monoid directly. We need Data.Monoid.Sum wrapper for it, and make it getSum $ mappend (Sum 1) (Sum 2) to get 3.

In the next post, we’ll do the same thing in a category of functors (Hask^Hask).