How to make two different modules implement the same 'interface'

I am trying to make a library that provides two different implementations of the same interface. But I am getting stuck with a puzzling error that tells me that my second implementation ‘doesn’t match’ the interface.

For the sake of simplicity I’ve boiled it down to something very simple. A ‘Box’ type that has just one funtion to make a Box.

file: box.mli

type 'a t
val make : 'a -> 'a t

file: box.ml

type 'a t = Box of 'a
let make x = Box x

So far everything is fine. This compiles, no errors and I think it does what I want.

Now for the second implementation I tried something like this:

file: altbox.mli

include module type of Box

file: altbox.ml

type 'a t = {contents: 'a}
let make x = {contents=x}

But I get an error like this:

File "lib/altbox.ml", line 1:       
Error: The implementation lib/altbox.ml
       does not match the interface lib/.playground.objs/byte/playground__Altbox.cmi:
        Type declarations do not match:
          type 'a t = { contents : 'a; }
        is not included in
          type 'a t = 'a Box.t
        The type 'a t is not equal to the type 'a Box.t
        File "lib/box.mli", line 1, characters 0-9: Expected declaration
        File "lib/altbox.ml", line 1, characters 0-26: Actual declaration

I think I need some way to tell the type checker that I don’t want Altbox.t to be the same type as Box.t but rather a brand new type of its own but just following the same signature patterns as the Box module (i.e isomorphic replacing Box.t with a brand new type). But I just can’t figure out how to do that.

Some things I tried:

altbox.mli

type 'a t
include module type of Box with type t = t

Error: A type variable is unbound ...

Okay, that make sense… I need to say 'a t

So I try:

type 'a t
include module type of Box with type 'a t = 'a t

Error:

File "lib/altbox.mli", line 2, characters 8-48:
2 | include module type of Box with type 'a t = 'a t
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: In this `with' constraint, the new definition of t
       does not match its original definition in the constrained signature:
       Type declarations do not match:
         type 'a t = 'a t
       is not included in
         type 'a t = 'a Box.t
       The type 'a t/2 is not equal to the type 'a t/1
       File "lib/box.mli", line 1, characters 0-9:
         Definition of type t/1
       File "lib/altbox.mli", line 1, characters 0-9:
         Definition of type t/2
       File "lib/box.mli", line 1, characters 0-9: Expected declaration
       File "lib/altbox.mli", line 2, characters 32-48: Actual declaration

I don’t understand this error or how to resolve it.

You can use a module type definition:

# module Box = struct
  module type S = sig
    type 'a t
    val make : 'a -> 'a t
  end

  module Impl : S = struct
    type 'a t = Box of 'a
    let make a = Box a
  end

  module Alt : S = struct
    type 'a t = { cont : 'a }
    let make a = { cont = a }
  end
end;;

# Box.[Impl.make 1; Alt.make 1];;
Error: This expression has type int Alt.t
       but an expression was expected of type int Impl.t

Is it possible to divide that up into different .mli / .ml files? I don’t really want to put all of my code into a single file to achieve this (the real code is quite a bit more complicated than a Box of 'a :slight_smile:

Also I’d like to somehow understand why what I tried didn’t work. Or if there’s some ‘magic syntax’ to make ‘isomorpic’ copy of a module type by doing a type subsitution (I thought that was what the with type syntax was… but… :confused:

In your example, 'a Box.t already has an implementation. And the compiler can’t prove that the 'a Altbox.t type is the same type as 'a Box.t, because it is an abstract type.

In the case of a module type definition, it doesn’t have an associated implementation, so we can annotate multiple modules with the signature. In a sense it is a freestanding signature.

You can definitely split up the definitions into multiple files. People usually do something like:

(* box_intf.ml *)
module type S = sig ... end

(* box.mli *)
include Box_intf.S

(* altbox.mli *)
include Box_intf.S
2 Likes

For context, the above box_intf.ml module uses a common pattern known as “The Intf trick”. The following blog post describes the pattern nicely:

I’ve been looking into this recently, trying to find some more beginner-friendly alternatives to this approach but nothing really ground-breaking so far.

Somewhat unrelated but I think these kind of tricks (everything around include or type of) are not helping readability of a code base. I understand that they come from a desire to minimise code repetition but there is a price of increasing indirection .

1 Like

I am surprised by the error given in the initial message : if I try to reproduce by just copying the files box.mli, box.ml, altbox.mli, altbox.ml, everything compiles fine with no error.

This is expected, because module type of Box does not strengthen the signature: there should not be the type equality type 'a t = 'a Box.t in the signature obtained for altbox.mli.

We can observe the behavior of module type of in the top-level:

# module type S = module type of Box;;
module type S = sig type 'a t val make : 'a -> 'a t end

The error given in the initial message would correspond to the signature strengthening we would obtained by querying the signature of the module struct include Box end.

# module type S = module type of struct include Box end;;
module type S = sig type 'a t = 'a Box.t val make : 'a -> 'a t end

Or, more simply, if 'a t was not left abstract in box.mli, but declared with its concrete representation: type 'a t = Box of 'a. @Kris_de_Volder, are you sure you compiled box.cmi and altbox.cmi with the code you provided?

I agree. It would be really nice if the core devs gave us some solution to this problem that didn’t require spaghetti.

1 Like