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:
If we allow computations to change this type, then we'll need both an input and an output state type for each computation:
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