Recall that when a signature doesn't specify a value, the value cannot be used outside a sealed module of that type. This applies to types as well. Let's use our example of ListStack from before.
module type StackSig = sig
type 'a stack
val empty : 'a stack
val push : 'a -> 'a stack -> 'a stack
val peek : 'a stack -> 'a
val pop : 'a stack -> 'a stack
end
module ListStack : StackSig = struct
type 'a stack = 'a list
let empty = []
let push x s = x :: s
let peek = function
| [] -> failwith "Empty"
| x :: _ -> x
let pop = function
| [] -> failwith "Empty"
| _ :: s -> s
end
module type StackSig =
sig
type 'a stack
val empty : 'a stack
val push : 'a -> 'a stack -> 'a stack
val peek : 'a stack -> 'a
val pop : 'a stack -> 'a stack
end
module ListStack : StackSig
We can see that ListStack.stack has a type of 'a list; however, if the module is sealed, we can't use list values with it:
let s1 : int ListStack.stack = ListStack.(empty |> push 42)
val s1 : int ListStack.stack = <abstr>
let s2 : int ListStack.stack = [42]
File "[3]", line 1, characters 31-35:
1 | let s2 : int ListStack.stack = [42]
^^^^
Error: This expression has type 'a list
but an expression was expected of type int ListStack.stack
Note how the second declaration failed. This is because type ListStack.stack isn't necessarily a list. We don't get to know or use the type of it. These two pieces of code would create the same structure, but the fact that the stack is a list is hidden behind the signature.
This is a good thing, because it lets us use the same signature for different implementations. You can think of a sealed class as a "private version," and an unsealed class a "public version".
Leaving the implementation off of a type is known a an abstract type. Abstract types give us encapsulation. Clients don't need to know how it works. Clients tend to exploit this, for example, say you want to upgrade to a two-list queue, you'd have broken code. This means that encapsulation is a good thing!
A common idiom for abstract types is to name the type t, for example:
module type Example = sig
type 'a t
val a_function: 'a t -> 'a t
end
module type Example = sig type 'a t val a_function : 'a t -> 'a t end
OCaml programmers tend to expect t to be the main implementation.