Subtype coercion with first-class modules

84 Views Asked by At

Given the following module signatures, where B subsumes A:

module type A = sig
  type t
end

module type B = sig
  type t
  val x: t
end

Normally, any module that satisfies B will then also satisfy A. This does not extend automatically to first-class modules however, because of course it's a different type language that doesn't allow automatic unification with subtypes. But manual coercion should still work, should it not? (It does in fact if the module signatures only contain type definitions, but not if it contains values.)

let f (_: (module A)) = ()
let g (b: (module B)) = f (b :> (module A)) (* Error: Type (module B) is not a subtype of (module A) *)

Is this a fundamental limitation of first-class modules? What are the rules that determine whether a module type is a subtype of another in the core type language? And is there a practical workaround?

2

There are 2 best solutions below

0
On BEST ANSWER

Type coercion is a type-level only operation in OCaml. In other words, it is expected to be a no-op at runtime.

To preserve this property, subtyping for first-class modules needs to use a restricted module subtyping relationship: (module A) <: (module B) if and only if A<:B and all runtime components of both module types appear at the same position.

For instance, this is fine:

module type A = sig
  type x = int
  type y = float
  val x : x
  val y :y
end
module type B = sig
  val x:int
  val y:float
end
let f (x:(module A) list) = (x:>(module B) list)

because both module types agrees that the first runtime component is x, and the second one is y. Contrarily

module type C = sig
  val y:int
  val x:float
end
let f (x:(module C) list) = (x:>(module B) list)

fails because the two module types disagrees on the position of the fields x and y.

Similarly, it is important to note that it is the position that matters,

module type With_hole = sig
  type t = int
  val x:float
  val intron: int;
  val y:int;
end
let f (x:(module With_hole) list) = (x:>(module B) list)

fails because the y field is at the third position in the With_hole module and at the second position in B. Beware that extensible variant constructor are values too:

type exn += X

create a runtime value (the extension constructor).

Lastly, in presence of submodules, all the submodules of A and B must be runtime-equivalent subtypes for A and B to be runtime-equivalent subtypes. In other words, this works:

module type A_nested = sig
  module M: A
end

module type B_nested = sig
  module M:B
end
let ok (x: (module A_nested)) = (x:> (module B_nested))

because the fields of the two submodules M are the same (same type and same position). But this doesn't:

module type C_nested = sig
  module M: C
end
let error (x:(module C_nested)) = (x :> (module B_nested))

since in order to convert C_nested into B_nested, we will need to permute the fields of the submodule M of C_nested.

0
On

To answer my own question on a practical workaround, it is of course possible to unpack and re-pack the module. And in this simple example that's quite convenient:

let g (module B: B) = f (module B)

It's somewhat less convenient when the first-class module is embedded in some data structure though.