Module Generalised_signatures.Monads

We begin with the classic monad signature, with a single type parameter:

module type Monad = sig ... end

This signature is enough to include many sensible monad instances. Unfortunately, it can't be used to describe types that are monadic only when partially applied, like result.

We could define a name for the partially-applied operator that specialises result to a particular error type:

(* Variant type holding the set of possible program errors *)
type error = ..
type 'a or_error = ('a, error) result

but life is too short to have each program define its own monad instance for its specific error cases. Instead, we can extend our Monad signature to carry an extra type parameter, held constant over each occurrence of t, that result can use to hold its error type. Since this extra type parameter isn't meaningful to the monad interface, we can consider it a phantom type parameter 'p.

This gives the following signature:

module type Monad_phantom = sig ... end

Now we can provide a result instance for our monad signature:

module Result_phantom : Monad_phantom with type ('o, 'e) t := ('o'e) Stdlib.result

We can go even further in adding type-level features to our monad signature. Some standard monad instances are like Result in being parameterised over some type p, but take this a step further by allowing p to change throughout the computation.

Consider the state monad, which threads a value of some type throughout a computation:

type ('a, 's) state = 's -> 'a * 's

If we allow computations to change this type, then we'll need both an input and an output state type for each computation:

type ('a, -'input, +'output) indexed_state = 'input -> 'a * 'output

Now when we sequence computations with ( >>= ) we just need to connect the inputs up with the outputs:

val bind :
  ('a, 'input, 'middle) t ->
  ('a -> ('b, 'middle, 'output) t) ->
  ('b, 'input, 'output) t

What we've described is sometimes called Atkey's indexed monad, and its interface captures even more sensible monad instances:

module type Monad_indexed = sig ... end

Now that we've pulled together some different type-level implementations of monads, we can write a generalisation of them:

module type Monad_generalised = sig ... end

From this, we can easily recover each of our monad variants:

module type Monad_simple' = sig ... end
module type Monad_indexed' = sig ... end
module type Monad_phantom' = sig ... end

Monad_generalised admits the common monad instances:

module Identity : Monad_generalised with type ('a, _, _, _) t := 'a
module List : Monad_generalised with type ('a, _, _, _) t := 'a list
module Result : Monad_generalised with type ('a, _, _, 'e) t := ('a'e) Stdlib.result
module Reader : Monad_generalised with type ('a, _, _, 's) t := 's -> 'a

Both forms of the state monad (indexed and non-indexed) add extra get and put operations to the monad interface:

module type State_generalised = sig ... end

Once again, we can recover the less general signatures with destructive substitution:

module type State_fixed' = sig ... end
module type State_indexed' = sig ... end