Monads

Recently I have been trying to understand monads again, and I finally feel I've made some progress, so I'm documenting it here. I will approach it from both the mathematical (category theory) point of view and the programming (Haskell) point of view.

How to find the right definition of a monad in Haskell

(Reference)

This section is not as precise and contains some intuitive thoughts for how to define a monad properly. Usually, your goal is to extend an arbitrary primitive computation f : a -> b in some way. Try to find the "extended type" that describes the extended computation. Here are some examples using some standard monads in Haskell.

In each case, the type on the right side of the a -> gives the right definition for the type of the monad. Thus a State monad has type (state -> (b, state)).

Monad Extension of f : a -> b Comments
Maybe a -> (b union Nothing) An additional value Nothing is allowed beyond whatever outputs were allowed in the primitive computation.
Reader a -> environment -> b The computation can depend on the environment, but cannot change the environment (the environment is read-only).
Writer a -> (b, extraOutput) Extra output (the "log") is produced, but the computation cannot depend on the extra output (the log is write-only)*.
State a -> state -> (b, state) The computation can depend on both the primitive input a and the state, and the output can yield a new state (both read and write are allowed for the state).
List a -> [b] The computation may produce any number (0,1,2,...) of values.
*Of course, it is possible to force the log into the input and thereby read it (see the pass/listen functions in the Writer monad), but it is nevertheless true that the situation we are trying to model is one where the output is "on the side" and does not affect the main computation a -> b.

The relationship between monads in category theory and Haskell

Category theory

The standard definition of a monad in category theory is as follows.

C1. A monad on a category C is a functor T : C -> C, together with 2 natural transformations η:id -> T and μ:T^2 -> T such that they obey certain "identity" and "associativity" laws:

Haskell

How does category theory apply to Haskell? The objects of the category are types, and the arrows between two objects (types) a and b are the functions a -> b. (Reference) So, the corresponding definition in Haskell would be as follows. Note that this is not how monads are actually implemented in Haskell; in Haskell, fmap and join are derived from the more primitive bind (>>=) operator. We will discuss the relationship between fmap, join, and bind later.

H1. A monad m induces a type m a for every type a, with the following operations defined on it:

  • fmap :: (a -> b) -> (m a -> m b)
  • return :: a -> m a
  • join :: m (m a) -> m a

    The 3 functions fmap, return, join correspond to the category theory T, η, and μ. The laws of monads are then written in Haskell notation as follows. We choose ○ to denote composition rather than the Haskell dot (.) so that it shows up clearer typographically. We place some type annotations as subscripts for clarity.

  • H1-1. (fmap is a functor) fmap ida = idm a
  • H1-2. (fmap is a functor) fmap (g ○ f) = (fmap g) ○ (fmap f) where f : a -> b and g : b -> c are two composable maps.
  • H1-3. (return is a natural transformation) (fmap fa -> b) ○ returna -> m a = returnb -> m b ○ f
  • H1-4. (join is a natural transformation) (fmap fa -> b) ○ joinm (m a) -> m a = joinm (m b) -> m b ○ (fmap (fmap f))
  • H1-5. (identity) join ○ fmap ○ returna -> m a = idm a
  • H1-6. (identity) join ○ returnm a -> m (m a) = idm a
  • H1-7. (associativity) joinm (m a) -> m a ○ fmap ○ joinm (m a) -> m a = join m (m a) -> m a ○ joinm (m (m a)) -> m (m a)

    Laws 3 and 4 correspond to the commutative square which defines a natural transformation between 2 functors F and G:

    Laws 5 and 6 correspond to the left and right identity laws (the left diagram in definition C1 above). Law 5 corresponds to going down, then right; law 6 corresponds to going right, then down.

    Law 7 is the associative law (the right diagram in definition C1 above). The left side corresponds to going right, then down; the right side corresponds to going down, then right.

    When performing calculations with fmap, it is more convenient to use a different notation. We write f# for (fmap f). In this notation, the laws above can be written more compactly:

  • H1-1'. id# = id
  • H1-2'. (g ○ f)# = g#○f#
  • H1-3'. f# ○ return = return ○ f
  • H1-4'. f# ○ join = join ○ f##
  • H1-5'. join ○ return# = id
  • H1-6'. join ○ return = id
  • H1-7'. join ○ join# = join ○ join

    Now let's consider another definition which reflects how monads are actually implemented in Haskell.

    H2. A monad m induces a type m a for every type a, with the following operations defined on it:

  • return :: a -> m a
  • m (>>=) f :: m a -> (a -> m b) -> m b

    They must obey the following laws.

  • H2-1. (return x) >>= f   =   f x
  • H2-2. m >>= return    =    m
  • H2-3. ( m >>= f ) >>= g    =    m >>= ( \x -> f x >>= g )

    Again, we define a more compact notation f* by f*(m) = m >>= f. In this new notation, the laws may be written without explicit arguments such as m and x, and thus appear in a simpler form:

  • H2-1'. f* ○ return = f
  • H2-2'. return* = id
  • H2-3'. (g* ○ f)* = g* ○ f*

    (We'll demonstrate the equivalence for the 2 versions of H2-3. (m >>= f) >>= g = g*(f*(m)) = (g* ○ f*)(m), while m >>= ( \x -> f x >>= g ) = m >>= (\x -> g*(f(x))) = (g* ○ f)*(m).)

    Claim: The definitions H1 and H2 are equivalent. To see this, note that both definitions have return in common, so we'll show how to derive fmap and join from >>=, and vica versa.

    To define >>= in terms of fmap and join

    Given f : a -> m b, we apply the functor fmap to get a map m a -> m (m b), then compose with join to get a map m a -> m b:

  • A1. m >>= f    =    join ( (fmap f) m)

    Or, in the compact notation:

  • A1'. f* = join ○ f#

    To define fmap and join in terms of >>=

    For fmap: Given a -> b, compose with returnb -> m b to get a map a -> m b, then apply >>= to get a map m a -> m b.

    For join: Given m (m a), bind with idm a to get a map m (m a) -> m a.

  • B1. (fmap f) m    =    m >>= (return ○ f)
  • B2. join m    =    m >>= id

    In the compact notation:

  • B1'. f# = (return ○ f)*
  • B2'. join = id*

    We must also show that the laws for each definition can be derived from the laws for the other.

    To derive H1 from H2

    We use H2-1' through H2-3' and B1' and B2' to derive H1-1' through H1-7' and A1'.

  • H1-1'. id# =(B1')= (return ○ id)* = return* =(H2-2')= id
  • H1-2'. g#○f# =(B1')= (return ○ g)* ○ (return ○ f)* =(H2-3')= ((return ○ g)* ○ return ○ f)* =(H2-1')= (return ○ g ○ f)* =(B1')= (g ○ f)#
  • H1-3'. f# ○ return =(B1')= (return ○ f)* ○ return =(H2-1') = return ○ f
  • H1-4'. f# ○ join =(B1')= (return ○ f)* ○ join =(B2')= (return ○ f)* ○ id* =(H2-3')= (return ○ f)** while join ○ f## =(B1',B2')= id* ○ (return ○ (return ○ f)*)* =(H2-3')= (id* ○ return ○ (return ○ f)*)* =(H2-1)' = (return ○ f)**
  • H1-5'. join ○ return# =(B1',B2')= id* ○ (return ○ return)* =(H2-3')= (id* ○ return ○ return)* =(H2-1')= return* =(H2-2')= id
  • H1-6'. join ○ return =(B2')= id* ○ return =(H2-1')= id
  • H1-7'. join ○ join# =(B1',B2')= id* ○ (return ○ id*)* =(H2-3')= (id* ○ return ○ id*)* =(H2-1')= id** while join ○ join =(B2')= id* ○ id* =(H2-3')= id**
  • A1'. join ○ f# =(B1')= join ○ (return ○ f)* =(B2')= id* ○ (return ○ f)* =(H3')= (id* ○ return ○ f)* =(H2-1')= f*

    To derive H2 from H1

    We use H1-1' through H1-7' and A1' to derive H2-1' through H2-3' and B1', B2'.

  • H2-1'. f* ○ return =(A1')= join ○ f# ○ return =(H1-3')= join ○ return ○ f =(H1-6')= f
  • H2-2'. return* =(A1')= join ○ return# =(H1-5')= id
  • H2-3'. (g* ○ f)* =(A1')= join ○ (g* ○ f)# =(A1')= join ○ (join ○ g# ○ f)# =(H1-2')= join ○ join# ○ g## ○ f# =(H1-7')= join ○ join ○ g## ○ f# =(H1-4')= join ○ g# ○ join ○ f# =(A1')= g* ○ f*
  • B1'. (return ○ f)* =(A1')= join ○ (return ○ f)# =(H1-2')= join ○ return# ○ f# =(H1-5')= f#
  • B2'. id* =(A1')= join ○ id# =(H1-1')= join

    Note that all of the laws of each set were used to derive the laws of the other.

    Kleisli composition

    The Kleisli composition operator, denoted in Haskell by (>=>), combines two functions f : a -> m b , g : b -> m c and produces a function a -> m c.
  • f >=> g = join ○ (fmap g) ○ f = join ○ g# ○ f

    It satisfies a true associative law, and return is a two-sided identity:

  • K1. f >=> return   =   return >=> f   =   f
  • K2. (f >=> g) >=> h   =   f >=> (g >=> h)

    These laws are easily verified using the previously stated laws. Using these facts, we may construct the Kleisli category whose objects are the types in Haskell (as before), but an arrow from a to b is defined to be a map a -> m b. Composition in the Kleisli category is >=>. The identity arrow on a in the Kleisli category is the return map sending a -> m a.

    A similar construction of the Kleisli category works in any monad over any category. There is an adjunction between the original category and the Kleisli category, and the monad defined by the adjunction is the monad we started with.

    The Kleisli category is one extreme of possible categories which produce an adjunction defining a given monad. The other extreme is the category of algebras for a monad (called a T-algebra if the monad has functor T). Reference: Maclane, Categories for the working mathematician, chapter 6.