Example: Arithmetic
Here is a module type that represents values that support the usual operations from arithmetic, or more precisely, a field:
module type Arith = sig
type t
val zero : t
val one : t
val (+) : t -> t -> t
val ( * ) : t -> t -> t
val (~-) : t -> t
end
There are a couple syntactic curiosities here. We have to write ( * )
instead
of (*)
because the latter would be parsed as beginning a comment. And
we write the ~
in (~-)
to indicate a unary negation operator.
Here is a module that implements that module type:
module Ints : Arith = struct
type t = int
let zero = 0
let one = 1
let (+) = Stdlib.(+)
let ( * ) = Stdlib.( * )
let (~-) = Stdlib.(~-)
end
Outside of the module Ints
, the expression Ints.(one + one)
is perfectly fine,
but Ints.(1 + 1)
is not, because t
is abstract: outside the module no one
is permitted to know that t = int
. In fact, the toplevel can't even give us
good output about what the sum of one and one is!
# Ints.(one + one);;
- : Ints.t = <abstr>
The reason why is that the type Ints.t
is abstract: the module type doesn't
tell use that Ints.t
is int
. This is actually a good thing in many cases:
code outside of Ints
can't rely on the internal implementation details of
Ints
, and so we are free to change it.
Since the Arith
interface only has functions that return t
, so once you
have a value of type t
, all you can do is create other values of type t
.
When designing an interface with an abstract type, you will almost certainly
want at least one function that returns something other than that type.
For example, it's often useful to provide a to_string
function. We could
add that to the Arith
module type:
module type Arith = sig
(* everything else as before, and... *)
val to_string : t -> string
end
And now we would need to implement it as part of Ints
:
module Ints : Arith = struct
(* everything else as before, and... *)
let to_string = string_of_int
end
Now we can write:
# Ints.(to_string (one + one));;
- : string = "2"