Here’s a type-checking problem I ran into today. I had a module with a variant type matching a signature that exposed the variant type.

module type S = sig
  type t = A | B
end

module M : S = struct
  type t = A | B
end

I wanted to extend the module with some new functions, and match a new signature that extended the original signature. Easy, right?

module type S' = sig
  include S
  val f : t -> t
end

module M' : S' = struct
  include M
  let f = function A -> B | B -> A
end

It was important to me to be able to include S in the new signature and include M in the new module to avoid duplicating code.

Then I hit a snag. As the code above stands, the two types, M.t and M'.t are different. We have a large codebase here at Jane Street, and there was some existing code that used the old module, M, and some other code that would use the new module M. I don’t want to change all of our code to use the new module, and I want our code to be able to interoperate – I don’t want two different types floating around.

Simple, right? Just use with type. That is, define S' as follows.

module type S' = sig
  include S with type t = M.t
  val f : t -> t
end

Unfortunately, that gives the following 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 t = M.t
is not included in
  type t = A | B

The with type would all work if we hadn’t exposed the variant in the original signature (check for yourself and see). But that’s not viable – I wanted to expose the variant.

I talked with some people here and we came up with a workaround, but I’d like to know if someone has a better one. Here’s our workaround.

module M = struct
  type t = A | B
end

module type S = sig
  type t = M.t = A | B
end

module type S' = sig
  include S val f : t -> t
end

module M' : S' = struct
  include M let f = function A -> B | B -> A
end