Losing type precision from module signature

2019-08-04 04:43发布

问题:

Let's say I had a simple module MyFoo that looks something like this

module MyFoo = struct
  type t =
    | A of string
    | B of int

  let to_string thing =
    match thing with
    | A str -> str
    | B n -> string_of_int n
end

With this definition, it works great and as expected — I can do something like

let _ = MyFoo.A "";;
- : MyFoo.t = MyFoo.A ""

without any problems.

Now maybe I want to create a functor that consumes modules with this structure, so I define a module signature that describes generally what this looks like and call it BaseFoo

module type BaseFoo = sig
  type t
  val to_string : t -> string
end

If I redefine MyFoo the same way but giving it this signature like

module MyFoo : BaseFoo = struct
  type t =
    | A of string
    | B of int

  let to_string thing =
    match thing with
    | A str -> str
    | B n -> string_of_int n
end

I lose the precision of its type t (is there a better way to describe what happens here?) — for example:

let _ = MyFoo.A "";;
Error: Unbound constructor MyFoo.A

What exactly is going on here and why does it happen? Is there a canonical way for dealing with this kind of problem (besides just leaving off the signature)?

I've tried manually including the signature and the specific type type definition too but get a different kind of error (this probably isn't the right approach).

module MyFoo : sig
  include BaseFoo
  type t = | A of string | B of int
end = struct
  type t =
    | A of string
    | B of int
  let to_string thing =
    match thing with
    | A str -> str
    | B n -> string_of_int n
end

let _ = MyFoo.A "test";;
Error: Multiple definition of the type name t.
       Names must be unique in a given structure or signature.

回答1:

You don't need the signature

What is going on is pretty much what you describe: giving MyFoo the BaseFoo signature in its definition restricts it to the signature.

Why? Because this is what specifying a signature at this place is for. The canonical solution is to leave of the signature (usually, letting the signature definition next to the module definition will be clear enough for the reader).

Note that when you call MyFoo on your functor, the signature will be checked. My usual choice is to rely on that.

A few workarounds

Given what you've tried, I guess this could be interesting to you:

module type BaseFoo = sig  ... end
module MyFoo = struct ... end

module MyFooHidden : BaseFoo = MyFoo (* Same as defining MyFoo : BaseFoo *)
module MyFooWithType :
   BaseFoo with type t = MyFoo.t
   = MyFoo (* What you want *)

The with type t = t' clause allows you to annotate a module signature to add type information to it. It is quite useful, especially when dealing with functors. See here for more information.

MyFooHidden may seem useless, but you can see it as a check that MyFoo has the right signature. You can still use MyFoo however you want after all. MyFooWithType is actually (a bit) less useful because if you change your signature to add a type you'd want exported, you'd need to add the export here too.

Using include

As for your include try. Well, nice try! You were almost there:

module MyFoo : sig
 type t = A of string | B of int
 include BaseFoo with type t := t
end

The with type t := t' is a bit different in that it doesn't perform an equality but a replacement. The type t definition is removed from the BaseFoo signature altogether and all instances are replaced with your own t, that way you don't get any double definition problem. See here for more details.

As you point out, this is probably not the approach you want, as you no longer easily know that MyFoo is indeed a BaseFoo.



标签: ocaml