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