Often when writing a new module, I want to write the interface first and save the implementation for later. This lets me use the module as a black box, extending the interface as needed to support the rest of the program. When everything else is finished, I can fill in the implementation, knowing the full interface I need to support. Of course sometimes the implementation needs to push back on the interface – this pattern isn’t an absolute – but it’s certainly a useful starting point. The trick is getting the program to compile at intermediate stages when the implementation hasn’t been filled in.
The longhand way I’ve used previously is update the .mli file as I go with the interface, and fill in stubs for each definition in the .ml file. For example, let’s say I’m writing a stack module:
(* stack.mli *)
type 'a t
val empty : 'a t
val push : 'a t -> 'a -> 'a t
(* stack.ml *)
type 'a t
let empty = failwith "unimplemented"
let push = failwith "unimplemented"
If I want to add a new value, I can just add a new stub:
(* stack.mli *)
type 'a t
val empty : 'a t
val push : 'a t -> 'a -> 'a t
val pop : 'a t -> ('a t * 'a) option
(* stack.ml *)
type 'a t
let empty = failwith "unimplemented"
let push = failwith "unimplemented"
let pop = failwith "unimplemented"
This works: I haven’t had to commit to a representation for stacks or an implementation for the operations, but my whole program can use the stack module and will still compile. Of course, nothing will run until I get rid of those exceptions. On the other hand, this is still more work than I’d like. I have to fill in stubs for every definition; in many cases, the stubs themselves are longer to write than their types in the .mli file. As my interface changes, I have to do as much work in the .ml file as I do in the .mli to keep the two in sync. It turns out I can do better. By rearranging the files, I can just update the interface as I go:
(* stack_intf.ml *)
module type S = sig
type 'a t
val empty : 'a t
val push : 'a t -> 'a -> 'a t
val pop : 'a t -> ('a t * 'a) option
end
(* stack.mli *)
include Stack_intf.S
(* stack.ml *)
include (val (failwith "unimplemented") : Stack_intf.S)
Now I can just add to stack_intf.ml as I go. Both stack.mli and stack.ml will take on the right module types automatically, without the need to add explicit stubs for type or value definitions. Once I’m done filling in the interface, I can paste the module type into stack.mli, remove stack_intf.ml, and start filling in stack.ml with real definitions. But the initial phase is much easier for only having to add exactly what I need.