Lecture 11
OCaml Modules
Programming Languages
UW CSE 341 - Winter 2022
Modules
For larger programs, one “top-level” sequence of bindings is poor
So ML has structures to define modules
module MyModule = struct bindings end
Example
module MyMathLib = struct
let rec fact x =
if x = 0
then 1
else x * fact (x - 1)
let half_pi = Float.pi /. 2.0
let doubler x = x *. 2
end
Implicit modules via files
OCaml also has a built-in convenience:
module Foo = struct … end
“around it”
Optional: Open
Can use open ModuleName to get “direct” access to a module’s bindings
Namespace management
So far, this is just namespace management
Giving a hierarchy to names to avoid shadowing
Module Types (aka Signatures)
module type MATHLIB = sig
val fact : int -> int
val half_pi : float
val doubler : int -> int
end
module MyMathLib : MATHLIB = struct
let rec fact x = ...
let half_pi = Float.pi /. 2.0
let doubler x = x *. 2
end
In general
module type SIGNAME = sig
types-for-bindings
end
module ModuleName : SIGNAME = struct
bindings
end
Hiding things
Real value of signatures is to to hide bindings and type definitions
Hiding implementation details is the most important strategy for writing correct, robust, reusable software
So first remind ourselves that functions already do well for some forms of hiding…
Hiding with functions
These three functions are totally equivalent: no client can tell which we are using (so we can change our choice later):
Defining helper functions locally is also powerful
Would be good to have “private” top-level functions too
let double x = x*2
let double x = x+x
let y = 2
let double x = x*y
Example
Outside the module, MyMathLib.doubler is simply unbound
module type MATHLIB = sig
val fact : int -> int
val half_pi : float
end
module MyMathLib : MYMATHLIB = struct
let rec fact x = ...
let half_pi = Float.pi /. 2.0
let doubler x = x *. 2
end
More OCaml pragmatics
A larger example [mostly see the code]
Now consider a module that defines an Abstract Data Type (ADT)
Our example: rational numbers with add and string_of_rational
module Rational1 = struct
type rational = Whole of int | Frac of int*int
exception BadFrac
(*internal functions gcd, reduce not on slide*)
let rec make_frac (n, d) = …
let rec add r1 r2 = …
let string_of_rational r = …
end
Library spec and invariants
Properties [externally visible guarantees, up to library writer]
Invariants [part of the implementation, not the module’s spec]
More on invariants
Our code guarantees the invariants and relies on them
Guarantee:
Rely:
A first signature
With what we know so far, this signature makes sense:
module type RATIONAL_A = sig
type rational = Whole of int | Frac of int * int
exception BadFrac
val make_frac : int * int -> rational
val add : rational -> rational -> rational
val string_of_rational : rational -> string
end
module Rational1 : RATIONAL_A = struct ...
The problem
By revealing the type definition, we let clients violate our invariants by directly creating values of type Rational1.rational
Any of these can lead to infinite loops or wrong results, which is why the module’s code would never return them:
module type RATIONAL_A = sig
type rational = Whole of int | Frac of int * int
...
So hide more
Key idea: An ADT must hide the concrete type definition so clients cannot create invariant-violating values of the type directly
Alas, this attempt doesn’t work because the signature now uses a type rational that is not known to exist:
module type RATIONAL_WRONG = sig
exception BadFrac
val make_frac : int * int -> rational
val add : rational -> rational -> rational
val string_of_rational : rational -> string
end
module Rational1 : RATIONAL_WRONG = struct ...
Abstract types
So OCaml has a feature for exactly this [common!!] situation:
In a signature: type foo
means the type exists, but clients do not know its definition
module type RATIONAL_B = sig
type rational
exception BadFrac
val make_frac : int * int -> rational
val add : rational -> rational -> rational
val string_of_rational : rational -> string
end
module Rational1 : RATIONAL_B = struct ...
This works! (And is a Really Big Deal)
Nothing a client can do to violate invariants and properties:
Using the module system to enforce abstractions
Two key restrictions
So we have two powerful ways to use signatures for hiding:
(Later we will see a signature can also make a binding’s type more specific than it is inside the module, which is cool but less important)
What about whole?
Must carefully reason about each public binding to ensure it does not break any abstractions
The whole function turns out to be fine
In fact, it would be okay to expose the Whole constructor as only Frac is a problem
Signature matching
Have so far relied on an informal notion of, “does a module type-check given a signature?” As usual, there are precise rules…
module Foo : BAR is allowed if:
Notice Foo can have more bindings (implicit in above rules)
Equivalent implementations
A key purpose of abstraction is to allow different implementations to be equivalent
Now: a second structure that can also have signature RATIONAL_A,
RATIONAL_B, or RATIONAL_C
(ignoring overflow)
Equivalent implementations
Example (see code file):
More interesting example
Given a signature with an abstract type, different structures can:
Such structures might or might not be equivalent
Example (see code):
Some interesting details for Rational3
Can’t mix-and-match module bindings
Modules with the same signatures still define different types
So things like this do not type-check (good!):
This is a crucial behavior for type system and module properties: