Claudiu Nistor
Claudiu Nistor

Reputation: 131

How to include a module inside another module while preserving their signatures?

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

Answers (1)

octachron
octachron

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

Related Questions