The Road to Odoc 3: Module Type Of
There are many new and improved features that Odoc 3 brings, but there are also a large number of bugfixes. I thought I'd write about one in particular here, an overhaul of "module type of" that landed in May 2024.Module type of
module type of
is a language feature of OCaml allowing
one to recover the signature of an existing module. For example, if I
had a module X:
module X = struct
# type t = Foo | Bar
end;;
module X : sig type t = Foo | Bar end
then I can get back the signature of X using
module type of
:
module type Xsig = module type of X
# module type Xsig = sig type t = Foo | Bar end
which can be very useful if you’re trying to extend existing modules amongst other things.
OCaml and Odoc treat module type of
in somewhat
different ways. OCaml internally expands the expression immediately it
sees it, and effectively replaces it with the signature - ie, in the
above example Xsig
is now a signature, not a
module type of
expression.
In contrast, Odoc would like to keep track of the fact that this
signature came from a module type of
expression, as it’s
very useful to know. If you’re extending a module, your signature might
look like:
module type ListExtended = sig
include module type of struct include List end
val extra_list_function : 'a list -> 'a list
end
The documentation we produce will expand the contents of the
include
statement, but keep track of the fact that it came
from a module type of
expression so the reader can see
where these signature items came from.
The problem
We run into difficulties as soon as we introduce another language
feature that operates on signatures: with
. Let’s start with
a module type S:
module type S = sig
module X : sig
type t
end
module type Y =
module type of struct include X end
end
We’ll now define a new module X2
that we intend to use
as a replacement for X
:
module X2 = struct
type t = int
type u = float
end
Now we’ll define a new module type T
which is
S
but with T
replaced:
module type T = S with module X := X2
When presented with this, OCaml expands the
module type of
expressions and tells us the computed
signatures:
$ ocamlc -i test.ml
module type S =
sig module X : sig type t end module type Y = sig type t = X.t end end
module X2 : sig type t = int type u = float end
module type T = sig module type Y = sig type t = X2.t end end
The interesting thing here is that in module type T
,
module type Y
only has a type t
in it, not a
type u
. As above, Odoc wants to keep the
module type of
expression so the reader can tell where
module type Y
came from. However, the substitution would do
a different thing in this case - we would have the following:
module type T = sig
module type Y = module type of struct include X2 end
end
and the expansion of this would then clearly have both types
t
and u
in it.
So now Odoc has two problems: We need to compute the correct signature, and we need to be able to describe how we computed it.
The previous solution to this was to have a ‘phase 0’ of odoc which
would compute the expansions of all module type of
expressions before doing any other work. This was necessary because of a
‘simplfying’ assumption in how we handled the typing environment. The
new, simpler approach was to calculate the expansion during the normal
flow of work, and never to attempt to recalculate it, but
simply operate on the signature. This was a nice big simplification and
optimisation that removed a few corner cases in the previous code
(including an infinite
loop that we hoped always terminated…!)
The second issue was how to describe it. We still want it clear that
this signature was derived from another, but it’s clear we can’t
honestly say that in the above example that it’s
module type of X2
. The answer is that we have applied a
transparent ascription to the signature. Essentially, the
signature is X2
but constrained to only have the fields of
X.
This is not a current feature of OCaml, though Jane Street has done some
work on this, including declaring the syntax:
X2 <: X
. However, there’s another interesting wrinkle
here. X
is a module defined in the module type
S
, so it’s not possible to write a valid OCaml path that
points to it – S.X
has no meaning. In addition, the
right-hand side of the <:
operator should be a module
type, so we’d actually need to write
X2 <: module type of S.X
. We’re still figuring out the
right thing to do here, so for now Odoc 3 will still pretend that it’s
simply module type of X2
.