Comparing Monads in Haskell and in OCaml
25 Feb 2021
1.
In the introduction part of the paper Lightweight Higher-kinded Polymorphism, the authors compared
two implementations of the when
function: one in OCaml and the other in Haskell.
The when
function as defined in Haskell is not simply a conditional with a
default. More importantly, the types of the branches are monadic.
For instance, the default branch return ()
(read "return unit") is a
higher-kinded polymorphic term of type Monad m => m ()
(read: "m unit, where
the type constructor m can be any monad", or simply "monad unit"); and return
itself has
type Monad m => a -> m a
(read: "a arrow m a, where the type constructor m can be any monad, and a can be any type", or simply "a arrow monad a").
In OCaml, the difficulty of implementing when
lies not in the conditional
but in the higher-kinded polymorphic return
function, for OCaml does not
support type variables of higher-kind: the higher-kinded type variables
(the m
's) in the signatures in the above paragraph have no parallel in OCaml.
The authors then point out that OCaml provides module functors to abstract
over type constructors, and show a way to implement when
in OCaml, subject
to the reader's know-how about implementing monads in OCaml.
I observe that the difference
between monads in OCaml and monads in Haskell is the root of the
difference between the when
functions.
2.
First let's see the similarity between monads in the two languages: why they are essentially the same so that we can call them both "monads"?
In OCaml, a monad is a module featuring an abstract unary type constructor with
an arbitrary name, say t
, and two monadic operators on t
which are return
and >>=
(read: "bind"). What is important about t
is not its name, but
its number of type parameters: 1, and the two operators on it: "return"
and "bind". This is exactly what is captured by the Monad
constructor class
in Haskell.
Now we compare the difference.
In Haskell, to declare a type constructor
as a monad, we simply need to declare it as an instance of
the Monad
class, and provide an implementation of the monadic operators
and perhaps check that the monadic laws hold.
While, in OCaml we begin with defining a module type Monad
consisting of
an abstract type constructor and the signatures of the two monadic operators;
the former (i.e., the abstract type constructor) corresponds in spirit to the constructor class parameter in Haskell, and the latter (i.e. the signatures
of the monadic operators) corresponds directly to the operator type
declarations in the Haskell constructor class. For each concrete type
constructor which we want to declare as a monad, we must define a module
in which we alias the type constructor and implement the
monadic operators in such a way that the module can be masked by the
Monad
module type.
3.
Such difference in monad representation entails that there is
one monadic return
function in Haskell which receives the type
Monad m => a -> m a
but there are infinitely many monadic
return
functions in OCaml each receiving a type where there is no polymorphism on
type constructors. This resembles the
contradiction between Church typing and Curry typing; for instance,
there is only one identity function by Curry typing, which receives a parametric polymorphic type, but there are infinitely many identity functions by Church
typing each receiving a concrete type.
It is because we have infinitely many monadic return
functions in OCaml
, each from a module that implements a specific monad, that we
have infinitely many when
functions in OCaml, each comes from an application
of the When
module functor to a module that implements a specific monad and
provides a specialized return
function.
4.
The interesting conclusion is that if a Haskell function has a higher-kinded polymorphic type, and we want to implement the function in OCaml, it is inevitable that we end up with a family of type-specific versions of the function defined by means of modules and module functors.
Follow-up Comments
Below are some follow-up comments on the blog.
The monadic return
is overloaded, or (higher-kinded) parametric polymorphic ?
03 March 2021; edited on 09 March 2021
In Section 1 of the blog, I wrote that the monadic return
function
is higher-kinded polymorphic. I later noticed that the authors of the
Lightweight Higher-kinded Polymorphism paper do not consider return
in
Haskell as being (higher-kinded) parametric polymorphic; instead, they
call it overloaded. Here I clarify the concepts.
Overloading and parametric polymorphism are two distinct
concepts: for a parametric polymorphic function its different type-specific
instances share exactly the same function definition, e.g., reversal of
a list is irrelevant to the list member type and the function definition
is the same for lists of characters, strings, integers, booleans or floats
etc.; for an overloaded function its different type-specific instances
definitely have different function definitions, e.g., the definition
of return
for a Maybe Monad is a "Maybe" wrapper, while for a List Monad
the definition of return
becomes a singleton-list wrapper. In other words,
an overloaded Haskell function and a parametric polymorphic function may have
type signatures that look similar: in both cases there are type variabes
that can be instantiated to obtain signatures of type-specific instances
of the function; however, a parametric polymorphic function has only one
concrete definition or embodiment, but an overloaded function has as many
concrete definitions or embodiments as the types for which the function
is overloaded.
Assigning a type to an overloaded function by means of a constructor class
(such as the Monad
class in Haskell) results in occurrences of
type variables in the signatures of certain functions; but occurrence of
instantiable type variables in the type signature does not imply that a
function so typed is necessarily parametric polymorphic.
To be more precise, the monadic return
function is a (higher-kinded)
ad-hoc polymorphic function (overloading = ad-hoc polymorphism), but
not a parametric polymorphic function.
It is therefore correct to say (albeit less precisely) that it is a
higher-kinded polymorphic function (as I say in Section 1).
The subtlety of checking type equality in OCaml
09 March 2021
Following a comparison of the when
function in Haskell with that in
OCaml, the authors comment that the extra heaviness of writing when
in OCaml
comes directly from the lack of overloading in the language; but more deeply (or more
generally) the reason is the lack of higher-kinded polymorphism.
I understand that return
is higher-kinded ad-hoc polymorphic. To have higher-kinded
ad-hoc polymorphism in OCaml, which is a special form of higher-kinded polymorphism,
we must provide what is required by any form of higher-kinded polymorphism, be it ad-hoc
or parametric; we must allow type variables of higher-kinds,
or some other way of abstracting over higher-kinded type constructors.
Then the authors move on to discuss the alias problem. They begin with the argument that checking type equality is easier in Haskell than in OCaml. I have some comments here.
I understand that in Haskell there is no way to hide a type equation (aka. a type synonym); only data constructors can be hidden and the programmer can choose to hide all or only some of the data constructors of an algebraic data type. But in OCaml, a type equation can be hidden; data constructors are exported in an all-or-nothing manner. In the above sense (and compared with Haskell) OCaml offers more flexibility for hiding type equations, but less flexibility for hiding data constructors.
The authors say that an OCaml module signature (aka. module type) may temporarily abstract a type in the case of applying a module functor. This is true and this is indeed a subtle technical point of OCaml modules. OCaml functors require that their argument modules must match the module types specified in the functor definitions. Explicit argument module types in a functor definition tells what the functor assumes about its argument modules, but these module types as functor argument specifications have no effect on what the type checker can see about the argument modules. A functor can assume about an argument module no more than what is given in the argument signature but outside the functor, the type checker could see the full picture of the argument module which is given by the module's own signature.
This blog post is also on GitHub with some extra code examples.