8
votes

I am reading through OCaml lead designer's 1994 paper on modules, types, and separate compilation. (kindly pointed to me by Norman Ramsey in another question ). I understand that the paper discusses the origins of OCaml's present module type / signature system. It it, the author proposes opaque interpretation of type declarations in signatures (to allow separate compilation) together with manifest type declarations (for expressiveness). Attempting to put together some examples of my own to demonstrate the kind of problems the OCaml module signature notation is trying to tackle I wrote the following code in two files:

In file ordering.ml (or .mli — I've tried both) (file A):

module type ORDERING = sig
 type t
 val isLess : t -> t -> bool
end

and in file useOrdering.ml (file B):

open Ordering
module StringOrdering : ORDERING
  let main () =
    Printf.printf "%b" StringOrdering.isLess "a" "b"
  main ()

The idea being to expect the compiler to complain (when compiling the second file) that not enough type information is available on module StringOrdering to typecheck the StringOrdering.isLess application (and thus motivate the need for the with type syntax). However, although file A compiles as expected, file B causes the 3.11.2 ocamlc to complain for a syntax error. I understood that signatures were meant to allow someone to write code based on the module signature, without access to the implementation (the module structure).

I confess that I am not sure about the syntax: module A : B which I encountered in this rather old paper on separate compilation but it makes me wonder whether such or similar syntax exists (without involving functors) to allow someone to write code based only on the module type, with the actual module structure provided at linking time, similar to how one can use *.h and *.c files in C/C++. Without such an ability it would seem to be that module types / signatures are basically for sealing / hiding the internals of modules or more explicit type checking / annotations but not for separate / independent compilation.

Actually, looking at the OCaml manual section on modules and separate compilation it seems that my analogy with C compilation units is broken because the OCaml manual defines the OCaml compilation unit to be the A.ml and A.mli duo, whereas in C/C++ the .h files are pasted to the compilation unit of any importing .c file.

3
As Thomas' answer momentarily said, there is no separate compilation by default for native compilation. I wish there were, and I entered a feature wish in mantis to this effect: caml.inria.fr/mantis/view.php?id=4389 . If anyone knows how to obtain separate native compilation in OCaml (as Thomas momentarily claimed was possible), I would be EXTREMELY interested in hearing about it. - Pascal Cuoq
@PascalCuoq: Why do you say that you cannot separately compile native code in Ocaml? Of course you can. - Andreas Rossberg
Actually, I just modified the two files as suggested in Thomas's answer and indeed you can compile separately either bytecode (ocamlc) or native (ocamlopt). - Marcus Junius Brutus
@AndreasRossberg There is no room to repeat my feature wish here, but look at the output of ocamldep, which corresponds to the actual dependencies between .cmx files if you do not move them around to prevent the compiler inlining stuff from one into the other. Note that the wish was resolved in "won't fix", not in "no change necessary". See also "separate compilation" remarks in frama-c.com/u3cat/download/CuoqICFP09.pdf . These have never been challenged. I am glad to here there is separate native compilation in OCaml but many people think there isn't. - Pascal Cuoq

3 Answers

6
votes

The right way to do such a thing is to do the following:

  1. In ordering.mli write:

    (* This define the signature *)
    module type ORDERING = sig
      type t
      val isLess : t -> t -> bool
    end
    
    (* This define a module having ORDERING as signature *)
     module StringOrdering : ORDERING
    
  2. Compile the file: ocamlc -c ordering.mli

  3. In another file, refer to the compiled signature:

    open Ordering
    
    let main () =
      Printf.printf "%b" (StringOrdering.isLess "a" "b")
    
    let () = main ()
    

    When you compile the file, you get the expected type error (ie. string is not compatible with Ordering.StringOrdering.t). If you want to remove the type error, you should add the with type t = string constraint to the definition of StringOrdering in ordering.mli.

So answer to you second question: yes, in bytecode mode the compiler just needs to know about the interfaces your are depending on, and you can choose which implementation to use at link time. By default, that's not true for native code compilation (because of inter-module optimizations) but you can disable it.

4
votes

You are probably just confused by the relation between explicit module and signature definitions, and the implicit definition of modules through .ml/.mli files.

Basically, if you have a file a.ml and use it inside some other file, then it is as if you had written

module A =
struct
  (* content of file a.ml *)
end

If you also have a.mli, then it is as if you had written

module A :
sig
  (* content of file a.mli *)
end =
struct
  (* content of file a.ml *)
end

Note that this only defines a module named A, not a module type. A's signature cannot be given a name through this mechanism.

Another file using A can be compiled against a.mli alone, without providing a.ml at all. However, you want to make sure that all type information is made transparent where needed. For example, suppose you are to define a map over integers:

(* intMap.mli *)
type key = int
type 'a map
val empty : 'a map
val add : key -> 'a -> 'a map -> 'a map
val lookup : key -> 'a map -> 'a option
...

Here, key is made transparent, because any client code (of the module IntMap that this signature describes) needs to know what it is to be able to add something to the map. The map type itself, however, can (and should) be kept abstract, because a client shouldn't mess with its implementation details.

The relation to C header files is that those basically only allow transparent types. In Ocaml, you have the choice.

3
votes

module StringOrdering : ORDERING is a module declaration. You can use this in a signature, to say that the signature contains a module field called StringOrdering and having the signature ORDERING. It doesn't make sense in a module.

You need to define a module somewhere that implements the operations you need. The module definition can be something like

module StringOrderingImplementation = struct
  type t = string
  let isLess x y = x <= y
end

If you want to hide the definition of the type t, you need to make a different module where the definition is abstract. The operation to make a new module out of an old one is called sealing, and is expressed through the : operator.

module StringOrderingAbstract = (StringOrdering : ORDERING)

Then StringOrderingImplementation.isLess "a" "b" is well-typed, whereas StringOrderingAbstract.isLess "a" "b" cannot be typed since StringOrderingAbstract.t is an abstract type, which is not compatible with string or any other preexisting type. In fact, it's impossible to build a value of type StringOrderingAbstract.t, since the module does not include any constructor.

When you have a compilation unit foo.ml, it is a module Foo, and the signature of this module is given by the interface file foo.mli. That is, the files foo.ml and foo.mli are equivalent to the module definition

module Foo = (struct (*…contents of foo.ml…*) end :
              sig (*…contents of foo.mli…*) end)

When compiling a module that uses Foo, the compiler only looks at foo.mli (or rather the result of its compilation: foo.cmi), not at foo.ml¹. This is how interfaces and separate compilation fit together. C needs #include <foo.h> because it lacks any form of namespace; in OCaml, Foo.bar automatically refers to a bar defined in the compilation unit foo if there is no other module called Foo in scope.

¹ Actually, the native code compiler looks at the implementation of Foo to perform optimizations (inlining). The type checker never looks at anything but what is in the interface.