0

With the Signature/Functor pattern, I refer to the style of Map.S / Map.Make in the OCaml standard library. This pattern is highly successful when you want to parameterize a large piece of code over some type without making it fully polymorphic. Basically, you introduce a parameterized module by providing a signature (usually called S) and a constructor (Make).

However, when you take a closer look, there is a lot of redundancy in the declaration:

  • First, both the signature and the functor have to be announced in the .mli file
  • Second, the signature has to be repeated completely in the .ml file (is there actually any legal way to differ from the .mli file here?)
  • Finally, the functor itself has to repeat all definitions again to actually implement the module type

Summa summarum, I get 3 definition sites for non-abstract types (e.g. when I want to allow pattern matching). This is completely ridiculous, and thus I assume there is some way around. So my question is two-fold:

  1. Is there a way to repeat a module type from an .mli file in an .ml file, without having to write it manually? E.g. something like ppx_import for module signatures?
  2. Is there a way to include a module type in a module inside an .ml file? E.g. when the module type has only one abstract type definition, define that type and just copy the non-abstract ones?
choeger
  • 3,562
  • 20
  • 33

4 Answers4

1
  • You can already use ppx_import for module signatures. You can even use it in a .ml to query the corresponding .mli.
  • If a module is composed only of module signatures, you can define the .mli alone, without any .ml. This way you can define a module, let's say Foo_sigs, containing the signature and use it everywhere else.
Drup
  • 3,679
  • 13
  • 14
  • i recently tried the second option and failed to make it build with ocamlbuild. could you post a minimal working example that builds with ocamlbuild? – user3240588 Mar 03 '15 at 10:37
  • 1
    Could you provide an example for the first option? How do I ppx_import a signature from the .mli? – choeger Mar 03 '15 at 11:00
  • for the first option, camlspotter answered. – Drup Mar 03 '15 at 14:31
1

Repeating type and module type definitions can be avoided to move them to external .ml file. Let's see the following example:

module M : sig

  (* m.mli *)    
  module type S = sig 
    type t 
    val x : t 
  end

  module type Result = sig
    type t
    val xs : t list
  end

  module Make(A : S) : Result with type t = A.t

end = struct

  (* m.ml *)
  module type S = sig 
    type t 
    val x : t 
  end

  module type Result = sig
    type t
    val xs : t list
  end

  module Make(A : S) = struct
    type t = A.t
    let xs = [A.x;A.x]
  end

end

Instead of writing two files m.mli and m.ml, I used a module M with an explicit signature: this is equivalent to have the two files and you can try it on OCaml toplevel by copy-and-paste.

In M, things are duped in sig .. end and struct .. end. This is cumbersome if module types become bigger.

You can share these dupes by moving them to another .ml file. For example, like the following n_intf.ml:

module N_intf = struct

  (* n_intf.ml *)    
  module type S = sig 
    type t 
    val x : t 
  end

  module type Result = sig
    type t
    val xs : t list
  end

end

module N : sig

  (* n.mli *)
  open N_intf
  module Make(A : S) : Result with type t = A.t

end = struct

  (* n.ml *)
  open N_intf

  module Make(A : S) = struct
    type t = A.t
    let xs = [A.x;A.x]
  end

end

You can also use *_intf.mli instead of *_intf.ml, but I recommend using *_intf.ml, since:

  • Module packing does not take mli only modules into account therefore you have to copy *_intf.cmi at installation.
  • Code generation from type definitions such as ppx_deriving needs things defined in .ml. In this example, it is no the case since there is no type definition.
camlspotter
  • 8,990
  • 23
  • 27
0

In that specific case, you can just skip the .mli part:

  • Your abstraction is specified by the .ml
  • Reading it makes it quite clear (as people know the pattern from the stdlib)
  • Everything that you'd put in the .mli is already in the .ml

If you work in a group that's requiring you to actually give a mli, just generate it automatically by using the ocamlc -i trick.

ocamlc -i m.ml >m.mli # automatically generate mli from ml

I know it doesn't exactly answer your question, but hey, it solves your problem.

I know that always putting a mli is considered to be best practice, but it's not mandatory, and that may be for some very good reasons.

As for your second question, I'm not sure I understood it well but I think this answers it:

module type ToCopy = sig type t val f : t -> unit end
module type Copy1 = sig include ToCopy with type t = int end
module type Copy2 = ToCopy with type t = int;;
PatJ
  • 5,996
  • 1
  • 31
  • 37
  • let's say i have other values in the same .ml whose signature i want to constrain. i can't just put those in an .mli and omit the signature of the module M in question in the .mli, since that would hide M to the outside, correct? so in that case, the solution does not seem ideal? – user3240588 Mar 03 '15 at 13:18
  • Correct, you can separate it in two distinct files though (or get the part of the mli you want automatically through a `-i` dump). I agree that some redundancy avoiding would be useful but it can in most times be dealt with through simple workarounds. – PatJ Mar 03 '15 at 13:24
0

Adding to camlspoter's answer and since the question mentions pattern matching, maybe you want to "re-export" the signatures and types with constructors declared in N_intf so they are accessible through N instead. In that case, you can replace the open's with include and module type of, i.e.:

module N_intf = struct

  type t = One | Two

  (* n_intf.ml *)    
  module type S = sig 
    type t 
    val x : t 
  end

  module type Result = sig
    type t
    val xs : t list
  end

end

module N : sig

  (* n.mli *)
  include module type of N_intf

  module Make(A : S) : Result with type t = A.t

end = struct

  (* n.ml *)
  include N_intf

  module Make(A : S) = struct
    type t = A.t
    let xs = [A.x;A.x]
  end

end

Then you'll get the following signatures:

module N_intf : 
  sig 
    type t = One | Two 
    module type S = sig type t val x : t end 
    module type Result = sig type t val xs : t list end
  end

module N : 
  sig         
    type t = One | Two
    module type S = sig type t val x : t end
    module type Result = sig type t val xs : t list end
    module Make : functor (A : S) -> sig type t = A.t val xs : t list end
  end

Now the constructors One and Two can be qualified by N instead of N_intf, so you can ignore N_intf in the rest of the program.

Ferd
  • 147
  • 8