Mismatched types between customized compilation units

Hello! I am trying to build a modularized system in OCaml, which consists of two compilation units named Instr and Monitor along with run_file.ml that makes use of these two modules.

Monitor has a function called run_mon that expects as input another function of type unit -> t and the unit value, then it returns a value of type c. Meanwhile, Instr has a function run_inst that expects a unit value and returns a function of type unit -> r. This type information is found in the interface files for the two modules mentioned above. The types t, c and r are abstract types inside the interface files for the modules. Inside the implementation files, type t = c = r = unit.

Inside run_file.ml, I open these two modules and simply use them as follows:

let run () = Monitor.run_mon (Instr.run_inst ()) ()

However, when trying to compile run_file.ml, the compiler raises an error, highlighting Instr.run_inst ():
Error: this expression has type unit → Instr.r but an expression was expected of type unit → Monitor.t
Type Instr.r is not compatible with type Monitor.t

I cannot understand why the compiler is complaining about this issue, however, I am quite new to OCaml so there is something I’m missing here. Any ideas on how to interpret the error and how this issue can be solved would be great.

My wild guess is that there are mli files for those modules and they don’t export the fact that r=c.

Hence the type error, the compiler cannot know outside of these modules that the two function types are equal.

But we would need a bit more details to be sure.

Yes, there are mli files for those modules, but the types in those mli files are abstract. So, for instance, the mli file for Instr contains the following:

type r

val run_inst: unit -> (unit -> r)

Then, in the ml file of this module is where I specify that r = unit. Similarly for the Monitor module.

Is this information hidden from the compiler? Have I done something wrong during compilation or should the implementation (ml) files make it clearer to the compiler in some way that r = c?

  • when you compile x.ml, the compiler doesn’t know about x.mli
  • when you compile y.ml or y.mli, the only facts known about module X are the ones in x.mli (not .ml)
  • you can only hide facts using a .mli, not add new ones

So how should the compilation process look like? I usually compile x.ml with x.mli, and similarly y.ml with y.mli, then in a file which uses them both I use the command:

ocamlc -c x.cmo y.cmo file.ml

The cmo files are not used at compilation time, only during linking or archiving. Thus ocamlc -c x.cmo y.cmo file.ml is functionally the same as ocamlc -c file.ml

The step-by-step process for compiling a compilation unit u.mli and u.ml :

  • compile the cmi files of the dependencies
  • compile u.mli into u.cmi
  • compile u.ml into u.cm{o,x}

Then once all cm{o,x} files have been built, they can be either archived into a cm{xa,a} library archive or linked into the final executable.

In other words, the mli file and the cmi describe the specification of your module to the external world, and when you write

type r
val run_inst: unit -> unit -> r

your specification tells the external world (aka anyone outside of instr.ml) that there is some type r which is produced by run_inst () (). Importantly, this implies that the external world cannot make any assumptions about the implementation of the type r. (And if there is no other function in the Instr module, it is impossible to use values of those types).

What does the whole compilation process look like? Someone can help you write a dune file for this if you’d like.

No, but it is hidden from clients. So run_file.ml cannot make use of the fact that in your implementation files you set the abstract types to unit. In particular Monitor.run_mon expects an arg of type Monitor.t, and that’s all. Clients are not allowed to “know” that monitor.ml defines t as unit.

Try this in the repl:

# module Monitor: sig
  type t
  val f: unit -> t
end = struct
  type t = unit
  let f = fun() -> ()
end;;
module Monitor : sig type t val f : unit -> t end

#show Monitor.f;;
val f : unit -> Monitor.t

# Monitor.f ();;
- : Monitor.t = <abstr>

Note that the compiler gets the type of Monitor.f from the signature, not the implementation.

See 5.2.4. Abstract Types · Functional Programming in OCaml