Obviously we'd like to have a stronger guarantee. OCaml gives us a way to achieve this. In the signature, we currently list the details of the type:
module type RATIONAL =
sig
type rational = Whole of int | Fraction of int * int
exception Not_a_rational
val make_fraction : int * int -> rational
val add : rational * rational -> rational
val to_string : rational -> string
end
We can instead just mention that a rational type will be defined without
specifying the details of how it is defined:
module type RATIONAL =
sig
type rational
exception Not_a_rational
val make_fraction : int * int -> rational
val add : rational * rational -> rational
val to_string : rational -> string
end
This is known as an abstract type. When we use this signature, a
client cannot see the Fraction constructor. Unfortunately, a client also can't
see the Whole constructor, which would require a client to say things like:
let x = Rational.make_fraction(23, 1);;
let y = Rational.make_fraction(27, 8);;
let z = Rational.add(x, y);;
This is fairly easy to fix. We can simply add a signature for the Whole
constructor in the RATIONAL signature:
module type RATIONAL =
sig
type rational
exception Not_a_rational
val make_fraction : int * int -> rational
val whole : int -> rational
val add : rational * rational -> rational
val to_string : rational -> string
end
We don't have to expose the details of the rational type to let OCaml
and clients know that there is something called whole that allows them
to construct a rational number from a single int. Because of the
naming rules of OCaml, we had to use a lowercase letter for whole
because with a capital letter Whole is assumed to be a constructor.
This allowed us to again write client code like the following:
let x = Rational.whole(23);;
let y = Rational.make_fraction(27, 8);;
let z = Rational.add(x, y);;
With these changes, we have guaranteed that clients must use either whole or
make_fraction to construct a rational number. That means that we have the
invariant we were looking for:
(* invariant: for any Fraction(a, b), b > 0 and gcd(a, b) = 1 *)
We still need to call reduce_rational in the add function because the
arithmetic involved in add can lead to a fraction that needs to be reduced, but
we don't have to call reduce_rational in functions like to_string because we
know that it's not possible for a client to construct a rational number that
violates our invariant.Here is the complete fourth version of the Rational structure:
(* Fourth version of Rational that further restricts the signature so that
the Fraction constructor is not exposed--finally we can guarantee
invariants because the client must use make_fraction *)
module type RATIONAL =
sig
type rational
exception Not_a_rational
val make_fraction : int * int -> rational
val whole : int -> rational
val add : rational * rational -> rational
val to_string : rational -> string
end
module Rational : RATIONAL =
struct
type rational = Whole of int | Fraction of int * int
exception Not_a_rational
let whole(i) = Whole(i)
let rec gcd(x, y) =
if x < 0 || y < 0 then gcd(abs(x), abs(y))
else if y = 0 then x
else gcd(y, x mod y)
let rec reduce_rational(r) =
match r with
| Whole(i) -> Whole(i)
| Fraction(a, b) ->
if b < 0 then reduce_rational(Fraction(-a, -b))
else let d = gcd(a, b)
in if b = d then Whole(a/d)
else Fraction(a/d, b/d)
(* client: please always construct fractions with this function *)
let make_fraction(a, b) =
if b = 0 then raise Not_a_rational
else reduce_rational(Fraction(a, b))
let add(r1, r2) =
match (r1, r2) with
| (Whole i, Whole j) -> Whole(i + j)
| (Whole i, Fraction(j, k)) -> Fraction(j + k * i, k)
| (Fraction(j, k), Whole i) -> Fraction(j + k * i, k)
| (Fraction(a, b), Fraction(c, d)) ->
reduce_rational(Fraction(a * d + c * b, b * d))
let to_string(r) =
match r with
| Whole i -> string_of_int(i)
| Fraction(a, b) -> string_of_int(a) ^ "/" ^ string_of_int(b)
end
I mentioned that using a signature with an abstract type, you can use a
completely different internal implementation and the client would never even
know it. For example, here is an alternative implementation of the signature
that implements rationals as a tuple of two ints:
(* Fifth version of Rational that reimplements the type using an int * int.
This change would be invisible (opaque) to a client of the structure. *)
module type RATIONAL =
sig
type rational
exception Not_a_rational
val make_fraction : int * int -> rational
val whole : int -> rational
val add : rational * rational -> rational
val to_string : rational -> string
end
module Rational : RATIONAL =
struct
type rational = int * int
exception Not_a_rational
let rec gcd(x, y) =
if x < 0 || y < 0 then gcd(abs(x), abs(y))
else if y = 0 then x
else gcd(y, x mod y)
let rec reduce_rational(a, b) =
if b < 0 then reduce_rational(-a, -b)
else let d = gcd(a, b)
in (a/d, b/d)
let make_fraction(a, b) =
if b = 0 then raise Not_a_rational
else reduce_rational(a, b)
let whole(a) = (a, 1)
let add((a, b), (c, d)) = reduce_rational(a * d + c * b, b * d)
let to_string(a, b) =
if b = 1 then string_of_int(a)
else string_of_int(a) ^ "/" ^ string_of_int(b)
end
This new structure provides the same functionality to a client as the
original and the client would have no way of telling them apart
because the signature uses an abstract type. This is a powerful and
useful mechanism.As a final example, I included a version that uses this new representation of a rational as a tuple and that uses a lazy approach rather than an eager approach to reducing a pair to its lowest terms. The previous versions call reduce_rational both in make_fraction and in add. Instead, we can wait until to_string is called to call reduce_rational because that's the first point in time when the client would notice that we hadn't reduced:
(* Sixth version of Rational that does a "lazy" reduce by only reducing
in toString *)
module type RATIONAL =
sig
type rational
exception Not_a_rational
val make_fraction : int * int -> rational
val whole : int -> rational
val add : rational * rational -> rational
val to_string : rational -> string
end
module Rational : RATIONAL =
struct
type rational = int * int
exception Not_a_rational
let rec gcd(x, y) =
if x < 0 || y < 0 then gcd(abs(x), abs(y))
else if y = 0 then x
else gcd(y, x mod y)
let rec reduce_rational(a, b) =
if b < 0 then reduce_rational(-a, -b)
else let d = gcd(a, b)
in (a/d, b/d)
let make_fraction(a, b) =
if b = 0 then raise Not_a_rational
else (a, b)
let whole(a) = (a, 1)
let add((a, b), (c, d)) = (a * d + c * b, b * d)
let to_string(a, b) =
let (a2, b2) = reduce_rational(a, b)
in if b2 = 1 then string_of_int(a2)
else string_of_int(a2) ^ "/" ^ string_of_int(b2)
end
The key point is not whether eager versus lazy computation is better. The key
point is that the client can't tell the difference, which means that the
implementor has the flexibility to choose either approach.