At what point do you start writing an .mli file?

This style doesn’t work very well with merlin unfortunately. The type errors that you get are hideous with the location spanning the entire signature.

1 Like

This style doesn’t work very well with merlin unfortunately. The type errors that you get are hideous with the location spanning the entire signature.

That’s true, I’ve learnt to work around it by fixing types as soon as
possible, and using assert false or similar things inside the module.
That’s actually another place where annotating functions is useful,
because is restricts the error to inside the function, instead of the
whole module.

1 Like

Wow, thanks for all the replies!

I can definitely see the arguments for both extremes. For someone like me, who is still new to OCaml, the .mli files have served as a nice set of “training wheels”; I get nicer and earlier errors when my implementation’s type signature doesn’t match what I had in mind. That advantage will probably become less relevant over time.

2 Likes

Since type-based disambiguation has been introduced in OCaml 4.01 (Using well-disciplined type-propagation to disambiguate label and constructor names · Issue #5759 · ocaml/ocaml · GitHub), it is quite common to write code that only type-checks with type annotations.

type t = A | B
type u = A | C
let f (x : t) : u = match x with A -> A | B -> C

More generally, I tend to annotate my let-definitions more and more systematically (at least the top-level ones): I think that this really improves error messages when type inference fails, and it helps for code review and rereading (it can be seen as a form of compiler-checked code documentation).

To answer to the initial question of the topic, I tend to write .mli files quite systematically, even for the entry-point module of an executable (in which case the .mli is just an empty file: it enables warnings 32, 34, 37 to detect unused definitions).

I usually write the definition first in .ml before writing the declaration and the documentation in .mli, because writing the code often helps me to find the right interface, especially when types become quite complex (with GADTs, typically). But I generally write a .mli file at least as soon as I have to use a definition from one module in another one.

When I have a lot of types to declare, I prefer to put them in a separate .ml file, without .mli, and in this case the .ml file contains the API documentation that would go otherwise in .mli files. Note that I could write them in a .mli file without implementation as well, except that it would prevent me to apply deriving mechanisms on these types, that I often want to use.

Functors are not a particular reason for writing .mlis, since signature compatibility is checked structurally anyway. On the contrary, module types are typically the kind of things I want to write in a .ml file without .mli, in order to not having to write them twice. An extreme case of that is when I need to manipulate the signature of a module Module as a module type: instead of writing the signature in module.mli and use module type of Module, I usually prefer to define a module type S in moduleS.ml (without moduleS.mli), and then module.mli is reduced to include ModuleS.S.

4 Likes

I think this benefit is worth repeating! It’s a really helpful way to find intentionally and accidentally unused code.

Hmmm… I feel the compiler should detect those unused definitions even without the .mli file.

Thierry is elite level, most people are not like that and don’t know everything he knows about ocaml.

Just noticed this thread. Very interesting to see some very experienced users saying that they don’t usually write mli files. I thought I was weird. For me, writing mli files often would partly defeat the purpose of using OCaml. I don’t want to have to write types in my code, and OCaml doesn’t make me do it. Yet I still get type safety.