jon.recoil.org

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.