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,
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
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