Reputation: 131
I'm trying to implement a small example of reusing modules inside other modules but so far I haven't succeeded fully.
What I'd like to achieve is to create 2 signatures - S and SE (with SE including S), and 2 modules - M (implementing S) and ME (implementing SE) with the purpose of not repeating previous code.
The bit I'm missing is including the contents of M inside ME:
module type S = sig
type t
val of_int : int -> t
end
module type SE = sig
include S
val to_string : t -> string
end
module M : S = struct
type t = int
let of_int x = x
end
module ME : SE = struct
(* include M *)
let to_string = string_of_int
end
Found a workaround so far by uncommenting (* include M *)
in ME and changing the definition of M to be module M : S with type t = int = struct
but that kind of defeats the purpose of this exercise as it changes the definition of M from implementing S to implementing something that looks like S.
Surely there must be a proper solution to this exercise. So what am I missing?
Upvotes: 1
Views: 370
Reputation: 18892
The issue is that once you have restricted the signature of M
to S
, there is not enough information left about M.t
to implement a meaningful to_string
function.
Indeed, a module of type S
defines a black-box (abstract type) t
that can be produced from an integer ... and that's all. In other words, you can only produce values of type M.t
but never have any clue on the content of thoses values. Thus, the only option left for to_string
is to ignore its argument, and return an unrelated string. For instance,
module ME: SE = struct
include M
let to_string _ = "?"
end
Defining M
and ME
in reverse order works better. First, we define the module with the more exhaustive API:
module ME : SE = struct
type t = unit
let of_int _ = ()
let to_string () = "()"
end
then we can erase the to_string
function with a signature constraint
module M: S = ME
The other option is to avoid making the type t
abstract, as you discovered, by using either the exact module type S with type t = int
or letting the compiler infers it.
In short, signature constraints are about information erasure: they allow to hide some information about an implementation. And hiding too much information can lead to useless modules.
Upvotes: 2