Question

I'd like to clarify one point: currently it seems to me that triple signature duplication is necessary while declaring a functor, provided we export it in the .mli file. Here is an example:

Suppose we have a functor Make, which produces a module A parametrized by SigA (simplest example I could think of). Consequently, in the .mli file we have:

module type A = sig
    type a
    val identity : a -> a
end

module type SigA = sig
    type a
end

module Make (MA:SigA) :
    A with type a := MA.a

Now I understand that we have to write an implementation in the .ml file:

module Make (MA:SigA) = struct
    type a = MA.a

    let identity obj = obj
end

So far so good, right? No! Turns out we have to copy the declaration of A and SigA verbatim into the .ml file:

module type A = sig
    type a

    val identity : a -> a
end

module type SigA = sig
    type a
end

module Make (MA:SigA) = struct
    type a = MA.a

    let identity obj = obj
end

While I (vaguely) understand the rationale behind copying SigA (after all, it is mentioned in the source code), copying A definition seems like a completely pointless exercise to me. I've had a brief look through the Core codebase, and they just seem to either duplicate it for small modules and for larger once they export it to the separate .mli, which is used both from .ml and .mli.

So is it just a state of affairs? Is everyone fine with copying the module signature THREE times (once in the .mli file, two times in the .ml file: declaration and the definition!!) Currently I'm considering just ditching .mli files altogether and restricting the modules export using signatures in the .ml files.


EDIT: yes I know that I can avoid this problem by declaring the interface for A inline inside Make in the .mli file. However this doesn't help me if I want to use that interface from outside of that module.

Was it helpful?

Solution

That's because a pair of ML and MLI file acts like a structure and a corresponding signature it is matched against.

The usual way to avoid writing out the module type twice is to define it in a separate ML file. For example,

(* sig.ml *)
module type A = sig
  type a
end

module type B = sig
  type b
  val identity : b -> b
end

(* make.mli *)
module Make (A : Sig.A) : Sig.B with type b = A.a

(* make.ml *)
module Make (A : Sig.A) =
struct
  type b = A.a
  let identity x = x
end

It is fine to leave out an MLI file in the case where it does not hide anything, like for the Sig module above.

In other cases, writing out the signature separately from the implementation is a feature, and not really duplication -- it defines the export of a module, and usually, that is a small subset of what's in the implementation.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top