The problem

OCaml compilation units live a double life: one as source code ( and one as header information (foo.mli).

This works well in encouraging abstraction, so you'll often see less type information in the .mli than in the .ml, but any types that are not abstracted are duplicated. This is a big deal for functor-heavy projects, since large module types will end up being duplicated across the two files.

There's a couple of standard mitigations for this:

  • move all of your types into a single file with no corresponding .mli.

    Each foo.{ml,mli} file can now alias the types and module types from there. Unfortunately, all those types are now defined separately from their point-of-use, making your codebase harder to understand and less scalable.

  • minimise the number of module types being defined.

    Since we're paying twice for each module type we define, it's natural to want to define as few of them as possible. For instance, we might avoid defining a MAKER type for our Make functor and just keep the constraints in the .mli file instead. Unfortunately, this hides the constraints from Merlin, so you won't discover any discrepancies until compile time:

(* --- foo.mli -------------------------------------------------------------- *)

module Make (A: Arg.S) : S with type arg = A.t

(* --- --------------------------------------------------------------- *)

module Make (A : Arg.S) = struct

  (* We must define [type arg = A.t], but Merlin doesn't know this *)
  type arg = string


Both of these mitigations have their drawbacks. If only our could refer to the module types defined in foo.mli. Hmm...

The solution (?)

As with most problems, we can solve this with another layer of indirection. We add a third file, named This file holds types and signatures, so is like our old foo.mli file, but has the distinct advantage that can pull types from it:

Now our types are defined in exactly one place, with no unnecessary duplication. The file contains all of the types required by and also defines a special module type Foo to act as the public interface.

(* --- ---------------------------------------------------------- *)

(* Type definitions go here: *)

module type S = sig ... end
module type MAKER = functor (A: Arg.S) -> S with type arg = A.t
type error = [ `Msg of string | `Code of int ]

(* The interface of []: *)

module type Foo = sig
  type error
  (** [error] is the type of errors returned by {!S}. *)

  module type S = S
  module type MAKER = MAKER

  module Make : MAKER

(* --- --------------------------------------------------------------- *)

(* Fetch module types and type definitions from the [_intf] file *)
include Foo_intf

(* Implementation here as normal *)
module Make : MAKER = functor (A : Arg.S) -> struct ... end

(* --- foo.mli -------------------------------------------------------------- *)

include Foo_intf.Foo (** @inline *)

There are some nice advantages to this approach:

  • We've avoided duplicate definitions of foo's module types and kept them in the foo* namespace in our source tree. The code is now easier to change (less repetition viscosity) and easier to understand.
  • Since we no longer have to minimise our use of module types, we can give the types of functors at the point of definition (module Make : MAKER = ...). This style works better with Merlin.

The _intf style is commonly used in Jane Street packages (c.f. higher_kinded, base, core). Note that it's typically only used for files that export module types.

Pro-tip #1: hiding _intf from Odoc

Use of the _intf trick is an implementation detail that (ideally) shouldn't be exposed in your documentation. Odoc renders includes and type aliases with links to the source definition. In the case of included module types, you can use the @inline annotation to prevent Odoc from displaying the indirection:

include Foo_intf.Foo (** @inline *)

Unfortunately: (a) there's no equivalent trick for plain type definitions, and (b) any cross-references between module types will link to the true definition. This leaves you with rendered output like the following:

module Make : functor (Input : Foo__.Foo_intf.INPUT) -> S

where INPUT is defined in the Foo_intf file but accessible to the user as Foo.INPUT (via an alias).

If anyone knows of a way to avoid this, please let me know! Perhaps in the future Odoc will have better ways to decide on the 'canonical' version of a type to display in the documentation (e.g. by using a cleverer inlining strategy or special-casing the _intf trick directly).

Pro-tip #2: faster file bootstrapping

An interesting side-effect of being able to reference interfaces from implementations is that you can use them to kick-start initial development on a file. If your development process begins by defining signatures, the .ml + .mli workflow requires a secondary step of "add stub implementations of everything" to sneak past the type-checker:

(* --- stack.mli ------------------------------------------------------------ *)

type 'a t
val empty : 'a t
val push : 'a t -> 'a -> 'a t
val pop : 'a t -> ('a t * 'a) option

(* --- ------------------------------------------------------------- *)

type 'a t
let empty = failwith "TODO"
let push = failwith "TODO"
let pop = failwith "TODO"

With an _intf file, we can provide all of these stubs in one go:

(* --- ------------------------------------------------------------- *)

include (val (failwith "TODO") : Stack_intf.Stack)

(I learned about this trick from a blog post by Carl Eastlund.)

Pro-tip #3: teaching Emacs about _intf files

Emacs users with tuareg-mode can use tuareg-find-alternate-file to quickly jump between corresponding .ml and .mli files. If you use this feature (as I do), you'll want it to be aware of _intf files. This can be done by customising the tuareg-find-alternate-file variable to include the correspondence <foo>.ml<foo>

;; Add support for `' ↔ `' in tuareg-find-alternate-file
    (("\\.mli\\'" (".ml" ".mll" ".mly"))
     ("\\'" (".ml"))
     ("\\.ml\\'" (".mli" ""))
     ("\\.mll\\'" (".mli"))
     ("\\.mly\\'" (".mli"))
     ("\\.eliomi\\'" (".eliom"))
     ("\\.eliom\\'" (".eliomi"))))))

If you're currently looking at some file, tuareg-find-alternate-file will try to open and then foo.mli in that order. (If one of the two already has an open buffer, that will take priority.)