University of Washington CSE 341: Programming Languages Unit 4 Reading Notes
University of Washington CSE 341: Programming Languages Unit 4 Reading Notes
Contents
Modules for Namespace Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Module Types (also known as Signatures) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Hiding Things . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Introducing our extended example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Module Types for Our Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
A Cute Twist: Expose the whole function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Rules for Checking Modules Against Module Types . . . . . . . . . . . . . . . . . . . . . . . . 8
Equivalent Implementations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Different modules define different types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
What is Type Inference? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Overview of OCaml Type Inference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
More Thorough Examples of OCaml Type Inference . . . . . . . . . . . . . . . . . . . . . . . . 13
Examples with Polymorphic Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
The Value Restriction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Some Things that Make Type Inference More Difficult . . . . . . . . . . . . . . . . . . . . . . 17
Mutual Recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Motivating and Defining Equivalence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Another Benefit of Side-Effect-Free Programming . . . . . . . . . . . . . . . . . . . . . . . . . 20
Standard Equivalences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Revisiting our Definition of Equivalence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1
we have been doing “at top-level” (i.e., outside of any module). Outside the module, you refer to a binding b
in Name by writing Name.b. We have already been using this notation to use functions like List.fold_left;
now you know how to define your own modules.
Though we will not do so in our examples, you can nest modules inside other modules to create a tree-shaped
hierarchy. In OCaml, it turns out even modules are first-class expressions, but we will be sticking with the
more basic features of the module system and most OCaml programs have no need to put modules in data
structures or pass them to/from functions. So for us, we can simplify the language by assuming modules
exist “around” the “core language” we have been studying so far.
If in some scope you are using many bindings from another module, it can be inconvenient to write
[Link] many times. Of course, you can use a regular binding to avoid this, e.g.,
let foo = [Link], but this technique is ineffective if we are using many different
bindings from the structure (we would need a new variable for each) or for using constructor names from the
module in patterns. So OCaml allows you to write open SomeLongStructureName, which provides “direct”
access (you can just write foo) to any bindings in the module that are mentioned in the module’s type. The
scope of an open is the rest of the enclosing structure (or the rest of the program at top-level).
A common use of open is to write succinct testing code for a module outside the module itself. Other uses
of open are often frowned upon because it may introduce unexpected shadowing, especially since different
modules may reuse binding names. For example, a list module and a tree module may both have functions
named map.
OCaml made the convenient and pragmatic choice that files implicitly define modules of the same (capital-
ized) name. So code in a file [Link] can just be a sequence of bindings and should not be surrounded by
any module keyword, yet to code in any other files, the code in [Link] is in the module Foo. Basically, there
is an implicit module Foo = struct ... end around the file.
2
let half_pi = [Link] / 2.0
let doubler y = y + y
end
Because of the : MATHLIB, the module MyMathLib will type-check only if it actually provides everything
MATHLIB claims it does and with the right types. Module types can also contain type definitions (including
variant types) and exception bindings. Because we check when we type-check MyMathLib that MATHLIB
accurates describes it, we can use this information when we check any code that uses MyMathLib. In other
words, we can just check clients assuming that the module type is correct.
Hiding Things
Before learning how to use OCaml modules to hide implementation details from clients, let’s remember that
separating an interface from an implementation is probably the most important strategy for building correct,
robust, reusable programs. Moreover, we can already use functions to hide implementations in various ways.
For example, all three of these functions double their argument, and clients (i.e., callers) would have no way
to tell if we replaced one of the functions with a different one:
let double1 x = x + x
let double2 x = x * 2
let y = 2
let double3 x = x * y
Another feature we use for hiding implementations is defining functions locally inside other functions. We
can later change, remove, or add locally defined functions knowing the old versions were not relied on by any
other code. From an engineering perspective, this is a crucial separation of concerns. I can work on improving
the implementation of a function and know that I am not breaking any clients. Conversely, nothing clients
can do can break how the functions above work.
But what if you wanted to have two top-level functions that code in other modules could use and have both
of them use the same hidden functions? There are ways to do this (e.g., create a record of functions), but it
would be convenient to have some top-level functions that were “private” to the module. In OCaml, there
is no “private” keyword like in other languages. Instead, you use module types that simply mention less:
anything not explicitly in a module type cannot be used from the outside. For example, if we change the
module type above to:
then client code cannot call [Link]. The binding simply is not in scope, so no use of it will
type-check. In general, the idea is that we can implement the module however we like and only bindings
that are explicitly listed in the module type can be called directly by clients.
OCaml’s pragmatic use of the file system to define modules extends to module types as follows: For the
module implicitly defined by [Link], you put the module type for it in [Link], but just the actual bindings.
For example, if [Link] contained:
3
let x = 13
let y = x + 1
val y : int
and [Link] would type-check and clients could use Foo.y (bound to 14), but Foo.x would be undefined
outside of [Link]. If instead, [Link] contained val y : string, then [Link] sould not type-check. If
there is not .mli file, then all top-level bindings are available to clients, i.e., the default is “nothing hidden.”
exception BadFrac
4
gcd x (y - x)
let reduce r =
match r with
| Whole _ -> r
| Frac (0, _) -> Whole 0
| Frac (n, d) ->
let g = gcd (abs n) d in
if g = d then
Whole (n / d)
else
Frac (n / g, d / g)
5
module type RATIONAL_A =
sig
type rational = | Frac of int * int | Whole of int
exception BadFrac
val make_frac : int * int -> rational
val add : rational -> rational -> rational
val string_of_rational : rational -> string
end
To use this module to hide gcd and reduce, we can just change the first line of the structure definition above
to module Rational1 : RATIONAL_A.
While this approach ensures clients do not call gcd or reduce directly (since they “do not exist” outside the
module), this is not enough to ensure the bindings in the module are used correctly. What “correct” means
for a module depends on the specification for the module (not the definition of the OCaml language), so let’s
be more specific about some of the desired properties of our library for rational numbers:
The properties are externally visible; they are what we promise clients. In contrast, the invariants are
internal ; they are facts about the implementation that help ensure the properties. The code above maintains
the invariants and relies on them in certain places to ensure the properties, notably:
gcd will violate the properties if called with an arguments ≤ 0, but since we know denominators are
> 0, reduce can pass denominators to gcd without concern.
string_of_rational and most cases of add do not need to call reduce because they can assume their
arguments are already in reduced form.
add uses the property of mathematics that the product of two positive numbers is positive, so we know
a non-positive denominator is not introduced.
Unfortunately, under module type RATIONAL_A, clients must still be trusted not to break the proper-
ties and invariants! Because the module type exposed the definition of the variant type definition, the
OCaml type system will not prevent clients from using the constructors Frac and Whole directly, by-
passing all our work to establish and preserve the invariants. Clients could make “bad” fractions like
[Link](1,0), [Link](3,-2), or [Link](9,6), any of which could then end up
causing gcd or string_of_rational to misbehave according to our specification. While we may have in-
tended for the client only to use make_frac, add, and string_of_rational, our module type allows more.
A natural reaction would be to hide the variant type by removing the line
type rational = | Frac of int * int | Whole of int. While this is the right intuition, the resulting
module type makes no sense and would be rejected: it repeatedly mentions a type rational that is not
known to exist. What we want to say instead is that there is a type rational but clients cannot know
anything about what the type is other than it exists. In a module type, we can do just that with an abstract
type, as this module type shows:
6
module type RATIONAL_B =
sig
type rational (* this is the line we changed to create an abstract type *)
exception BadFrac
val make_frac : int * int -> rational
val add : rational * rational -> rational
val string_of_rational : rational -> string
end
(Of course, we also have to change the first line of the module definition to use this module type instead.
That is always true, so we will stop mentioning it.)
This new feature of abstract types, which makes sense only in module types, is exactly what we want. It lets
our module define operations over a type without revealing the implementation of that type. The syntax is
just to give a type binding without a definition. The implementation of the module is unchanged; we are
simply changing how much information clients have.
Now, how can clients make rationals? Well, the first one will have to be made with make_frac. After that,
more rationals can be made with make_frac or add. There is no other way, so thanks to the way we wrote
make_frac and add, all rationals will always be in reduced form with a positive denominator.
What RATIONAL_B took away from clients compared to RATIONAL_A is the constructors Frac and Whole. So
clients cannot create rationals directly and they cannot pattern-match on rationals. They have no idea how
they are represented internally. They do not even know rational is implemented as a variant type.
Abstract types are a Really Big Deal in programming.
7
Rules for Checking Modules Against Module Types
So far, our discussion of whether a module “should type-check” given a particular module type has been
rather informal. Let us now enumerate more precise rules for what it means for a module to match a module
type. (This terminology has nothing to do with pattern-matching.) If a module does not match a module
type assigned to it, then the module does not type-check. A module Name matches a module type BLAH if:
For every val-binding in BLAH, Name must have a binding with that type or a more general type (e.g.,
the implementation can be polymorphic even if the module type says it is not — see below for an
example).
For every non-abstract type-binding in BLAH, Name must have the same type binding.
For every abstract type-binding in BLAH, Name must have some binding that creates that type (it could
be a variant type, a record type, or a type synonym).
For every exception binding in BLAH, Name must have the same exception binding (same name and
carrying the same data, if any).
Notice that Name can have any additional bindings that are not in the module type. This is implicit in the
definition above.
Equivalent Implementations
Given our property- and invariant-preserving signatures RATIONAL_B and RATIONAL_C, we know clients cannot
rely on any helper functions or the actual representation of rationals as defined in the module. So we could
replace the implementation with any equivalent implementation that had the same properties: as long as
any call to the string_of_rational binding in the module produced the same result, clients could never
tell. This is another essential software-development task: improving/changing a library in a way that does
not break clients. Knowing clients obey an abstraction boundary, as enforced by OCaml’s signatures, is
invaluable.
As a simple example, we could make gcd a local function defined inside of reduce and know that no client
will fail to work since they could not rely on gcd’s existence. More interestingly, let’s change one of the
invariants of our structure. Let’s not keep rationals in reduced form. Instead, let’s just reduce a rational
right before we convert it to a string. (This example ignores the issue of overflow; please excuse this fiction.)
This simplifies make_frac and add, while complicating string_of_rational, which is now the only function
that needs reduce. Here is the whole structure, which would still match signatures RATIONAL_A, RATIONAL_B,
or RATIONAL_C:
exception BadFrac
8
raise BadFrac
else if d < 0 then
Frac (-n, -d)
else
Frac (n, d)
(* no call to reduce! *)
let rec add r1 r2 =
match (r1, r2) with
| Whole i1, Whole i2 -> Whole (i1 + i2)
| Whole i1, Frac (n2,d2) -> Frac ((i1 * d2) + n2, d2)
| Frac _, Whole _ -> add r2 r1
| Frac (n1,d1), Frac (n2,d2) -> Frac ((n1 * d2) + (n2 * d1), d1 * d2)
let string_of_rational r =
let rec gcd x y =
if x = y then
x
else if x > y then
gcd y x
else
gcd x (y - x)
in
let reduce r =
match r with
| Whole _ -> r
| Frac (0, _) -> Whole 0
| Frac (n, d) ->
let g = gcd (abs n) d in
if g = d then
Whole (n / d)
else
Frac (n / g, d / g)
in
match reduce r with
| Whole i -> string_of_int i
| Frac (n, d) -> string_of_int n ^ "/" ^ string_of_int d
end
If we give Rational1 and Rational2 the module type RATIONAL_A, both will type-check, but clients can
still distinguish them. For example, Rational1.string_of_rational ([Link](21,3)) produces
"21/3", but Rational2.string_of_rational ([Link](21,3)) produces "7". But if we give
Rational1 and Rational2 the module type RATIONAL_B or RATIONAL_C, then the modules are equivalent
for any possible client. This is why it is important to use restrictive module types like RATIONAL_B to begin
with: so you can change the module later without checking all the clients.
While our two modules so far maintain different invariants, they do use the same definition for the type
rational. This is not necessary with module types RATIONAL_B or RATIONAL_C; a different module having
these module types could implement the type differently. For example, suppose we realize that special-casing
whole-numbers internally is more trouble than it is worth. We could instead just use int*int and define
this module:
9
module Rational3 = struct
type rational = int * int
exception BadFrac
end
(This module takes the Rational2 approach of having string_of_rational reduce fractions, but that issue
is largely orthogonal from the definition of rational.)
Notice that this module provides everything RATIONAL_B requires. The function make_frac is interesting
in that it takes an int*int and returns an int*int, but clients do not know the actual return type, only
the abstract type rational. And while giving it an argument type of rational in the module type would
match, it would make the module useless since clients would not be able to create a value of type rational.
Nonetheless, clients cannot pass just any int*int to add or string_of_rational; they must pass something
that they know has type rational. As with our other modules, that means rationals are created only by
make_frac and add, which enforces all our invariants.
10
Our modulee does not match RATIONAL_A since it does not provide rational as a variant type with con-
structors Frac and Whole.
Our module does match module type RATIONAL_C because we have a function whole of the right type.
The fact that let whole i = (i,1) matches val whole : int -> rational is interesting. The type
of whole in the module is actually polymorphic: ’a -> ’a * int and it could be used with any type
internally. OCaml module type checking allows ’a -> ’a * int to match int -> rational because
’a -> ’a * int is more general than int -> int * int and int -> rational is a correct abstraction
of int -> int * int. Less formally, the fact that whole has a polymorphic type inside the module does
not mean the module type has to give it a polymorphic type outside the module. And in fact it cannot while
using the abstract type since whole cannot have the type ’a -> int * int or ’a -> rational.
11
say type inference may be impossible, we mean this in the technical sense of undecidability, like the famous
halting problem. We mean there are type systems for which no computer program can implement type
inference such that (1) the inference process always terminates, (2) the inference process always succeeds if
inference is possible, and (3) the inference process always fails if inference is not possible.
Fortunately, OCaml was rather cleverly designed so that type inference can be performed by a fairly straight-
forward and elegant algorithm. While there are programs for which inference is intractably slow, programs
people write in practice never cause such behavior. We will demonstrate key aspects of the algorithm for
OCaml type inference with a few examples. This will give you a sense that type inference is not “magic.”
For simplicity, we will not describe the full algorithm nor write code to implement it.
OCaml type inference ends up intertwined with parametric polymorphism — when the inferencer determines
a function’s argument or result “could be anything” the resulting type uses ’a, ’b, etc. But type inference
and polymorphism are entirely separate concepts: a language could have one or the other. For example,
Java has generics but no inference for method argument/result types.
It determines the types of bindings in order, using the types of earlier bindings to infer the types of
later ones. This is why you cannot use later bindings in a file. (When you need to, you use mutual
recursion and type inference determines the types of all the mutually recursive bindings together.
Mutual recursion is covered later in this unit.)
For each let-binding, it analyzes the binding to determine necessary facts about its type. For example,
if we see the expression x+1, we conclude that x must have type int. We gather similar facts for
function calls, pattern-matches, etc.
Afterward, use type variables (e.g., ’a) for any unconstrained types in function arguments or results.
(Enforce the value restriction — only variables and values can have polymorphic types, as discussed
later. OCaml tries to minimize the annoyance of this by “looking ahead” to find a non-polymorphic
type to choose where possible.)
The amazing fact about the OCaml type system is that “going in order” this way never causes us to reject a
program that could type-check nor do we ever accept a program we should not. So explicit type annotations
really are optional.
Here is an initial, very simple example:
let x = 42
let f (y,z,w) = if y then z+x else 0
Type inference first gives x type int since 42 has type int. Then it moves on to infer the type for f. Next
we will study, via other examples, a more step-by-step procedure, but here let us just list the key facts:
z must have type int because we add it to something we already determined has type int.
12
f must return an int because its body is a conditional where both branches return an int. (If they
disagreed, type-checking would fail.)
let f x =
let (y,z) = x in
(abs y) + z
Looking at the first line, f must have type T1->T2 for some types T1 and T2 and in the function body
f has this type and x has type T1.
Looking at the let-binding, x must be a pair type (else the pattern-match makes no sense), so in fact
T1=T3*T4 for some T3 and T4, and y has type T3 and z has type T4.
Looking at the addition expression, we know from the static environment that abs has type int->int,
so y having type T3 means T3=int. The other argument to + must also have type int, so z having
type T4 means T4=int.
Since the type of the addition expression is int, the type of the let-expression is int. And since the
type of the let-expression is int, the return type of f is int, i.e., T2=int.
Putting all these constraints together, T1=int*int (since T1=T3*T4) and T2=int, so f has type int*int->int.
Next example:
From the first line, there exists types T1 and T2 such that sum has type T1->T2 and xs has type T1.
Looking at the match-expression, xs must have a type that is compatible with all of the patterns.
Looking at the patterns, both of them match any list, since they are built from list constructors (in the
x::xs’ case the subpatterns match anything of any type). So since xs has type T1, in fact T1=T3 list
from some type T3.
Looking at the right-hand sides of the match-expression branches, we know they must have the same
type as each other and this type is T2. Since 0 has type int, T2=int.
13
Looking at the second branch, we type-check it in a static environment where x and xs’ are available.
Since we are matching the pattern x::xs’ against a T3 list, it must be that x has type T3 and xs’
has type T3 list.
Now looking at the right-hand side, we add x, so in fact T3=int. Moreover, the recursive call type-
checks because xs’ has type T3 list and T3 list=T1 and sum has type T1->T2. Finally, since T2=int,
adding sum xs’ type-checks.
Putting everything together, we get sum has type int list -> int.
Notice that before we got to sum xs’ we had already inferred everything, but we still have to check that types
are used consistently and reject otherwise. For example, if we had written sum x, that cannot type-check —
it is inconsistent with previous facts. Let us see this more thoroughly to see what happens:
Type inference for broken_sum proceeds largely the same as for sum. The first four bullets from the
previous example all apply, giving broken_sum type T3 list ->int, x3 type T3 list, x type T3, and
xs’ type T3 list. Moreover, T3=int.
We depart from the correct sum implementation with the call broken_sum x. For this call to type-
check, x must have the same type as broken_sum’s parameter, or in other words, T1=T3. However, we
know that T1=T3 list, so this new constraint T1=T3 actually generates a contradiction: T3=T3 list.
If we want to be more concrete, we can use our knowledge that T3=int to rewrite this as int=int list.
Looking at the definition of broken_sum, it should be obvious that this is exactly the problem: we
tried to use x as an int and as an int list.
When your OCaml program does not type-check, the type-checker reports the expression where it discovered
a contradiction and what types were involved in that contradiction. While sometimes this information is
helpful, other times the actual problem is with a different expression, but the type-checker did not reach a
contradiction until later.
14
xs has type T1.
T1=T3 list (due to the pattern-match)
T2=int because 0 can be the result of a call to length.
x has type T3 and xs’ has type T3 list.
The recursive call length xs’ type-checks because xs’ has type T3 list, which is T1, the argument
type of length. And we can add the result because T2=int.
So we have all the same constraints as for sum, except we do not have T3=int. In fact, T3 can be anything
and length will type-check. So type inference recognizes that when it is all done, it has length with type
T3 list -> int and T3 can be anything. So we end up with the type ’a list -> int, as expected. Again
the rule is simple: for each Ti in the final result that cannot be constrained, use a type variable.
A second example:
Given the two curried arguments to compose, compose has type T1->T2->T3, f has type T1 and g has
type T2 for some T1, T2, and T3.
Since compose returns a function, T3 is some T4->T5 where in that function’s body, x has type T4.
So g must have type T4->T6 for some T6, i.e., T2=T4->T6.
And f must have type T6->T7 for some T7, i.e., T1=T6->T7.
But the result of f is the result of the function returned by compose, so T7=T5 and so T1=T6->T5.
Putting together T1=T6->T5 and T2=T4->T6 and T3=T4->T5 we have a type for compose of
(T6->T5) -> (T4->T6) -> (T4->T5). There is nothing else to constrain the types T4, T5, and T6, so we
replace them consistently to end up with (’a->’b)*(’c->’a) -> (’c->’b) as expected (and the last set
of parentheses are optional, but that is just syntax).
Here is a simpler example that also has multiple type variables:
let f (x,y,z) =
if true then
(x,y,z)
else
(y,x,z)
The first line requires that f has type T1*T2*T3 -> T4, x has type T1, y has type T2, and z has type
T3.
The two branches of the conditional must have the same type and this is the return type of the function
T4. Therefore, T4=T1*T2*T3 and T4=T2*T1*T3. This constraint requires T1=T2.
Putting together these constraints (and no others), f will type-check with type T1*T1*T3 -> T1*T1*T3 for
any types T1 and T3. So replacing each type consistently with a type variable, we get ’a*’a*’b -> ’a*’a*’b,
which is correct: x and y must have the same type, but z can (but need not) have a different type. Notice
that the type-checker always requires both branches of a conditional to type-check with the same type, even
though here we know which branch will be evaluated.
15
The Value Restriction
This section is less important for understanding the key ideas behind type inference, but we include it because
without it we aren’t telling enough of the truth about OCaml and type inference.
As described so far in this unit, the OCaml type system is unsound, meaning that it would accept programs
that when run could have values of the wrong types, such as putting an int where we expect a string. The
problem results from a combination of polymorphic types and mutable references, and the fix is a special
restriction to the type system called the value restriction.
This is an example program that demonstrates the problem:
Straightforward use of the rules for type checking/inference would accept this program even though we
should not – we end up trying to add 1 to "hi". Yet everything seems to type-check given the types for the
functions/operators ref (’a -> ’a ref), := (’a ref -> ’a -> unit), and ! (’a ref -> ’a).
To restore soundness, we need a stricter type system that does not let this program type-check – we need
at least one of the lines above not to type-check. The choice OCaml made is to prevent the first line from
having a polymorphic type. Therefore, the second and third lines will not both type-check because they will
not be able to instantiate an ’a with string and int. In general, OCaml will give a variable in a let-binding
a polymorphic type only if the expression in the let-binding is a value or a variable. This is called the value
restriction. In our example, ref None is a call to the function ref. Function calls are not variables or values.
So we get a strange result that will make sure subsequent code uses r with at most one non-polymorphic
type.
It is not at all obvious that this restriction suffices to make the type system sound, but in fact it is sufficient.
As we saw previously when studying partial application, the value restriction is occasionally burdensome
even when it is not a problem because we are not using mutation. We saw that this binding falls victim to
the value-restriction and is not made polymorphic:
There are various workaround, with the simplest being to wrap the call to [Link] in a function —
this would be unnecessary function wrapping if not for the value restriction, but here it is necessary to get
a polymorphic type.
One might wonder why we cannot enforce the value restriction only for references (where we need it) and
not for immutable types like lists. The answer is the OCaml type-checker cannot always know which types
are really references and which are not. In the code below, we need to enforce the value restriction on the
last line, because ’a foo and ’a ref are the same type.
Because of OCaml’s module system, the type-checker does not always know the definition of type synonyms
(recall this is a good thing — abstract types!). So to be safe, it enforces the value restriction for all types.
16
The reason why wrapping in a function “works” for references is it means we would make a new reference
every time we called the function. Each one can be instantiated to a different type.
Inference would be more difficult if OCaml had subtyping (e.g., if every triple could also be a pair)
because we would not be able to conclude things like, “T3=T1*T2” since the equals would be overly
restrictive. We would instead need constraints indicating that T3 is a tuple with at least two fields.
Depending on various details, this can be done, but type inference is more difficult and the results are
more difficult to understand.
Inference would be more difficult if OCaml did not have parametric polymorphism since we would have
to pick some type for functions like [Link] and compose and that could depend on how they are
used.
Mutual Recursion
We have seen many examples of recursive functions and many examples of functions using other functions
as helper functions, but what if we need a function f to call g and g to call f? That can certainly be useful,
but OCaml’s rule that bindings can only use earlier bindings makes it more difficult — which should come
first, f or g?
It turns out OCaml has special support for mutual recursion using the keyword and and putting the mutually
recursive functions next to each other. Similarly, we can have mutually recursive type bindings. After showing
these new constructs, we will show that you can actually work around a lack of support for mutually recursive
functions by using higher-order functions, which is a useful trick in general and in particular in OCaml if
you do not want your mutually recursive functions next to each other.
Our first example uses mutual recursion to process an int list and return a bool. It returns true if the
list strictly alternates between 1 and 2, starts with a 1, and ends with a 2 (or is empty). There are many
ways to implement such a function, but our approach does a nice job of having for each “state” (such as “a
1 must come next” or “a 2 must come next”) a function. In general, many problems in computer science
can be modeled by such finite state machines, and mutually recursive functions, one for each state, are an
elegant way to implement finite state machines.1
let alt_end_in_two xs =
let rec s_need_one xs =
match xs with
| [] -> true
| 1::xs’ -> s_need_two xs’
| _ -> false
and s_need_two xs =
match xs with
| [] -> false
| 2::xs’ -> s_need_one xs’
1 Because all function calls are tail calls, the code runs in a small amount of space, just as one would expect for an imple-
17
| _ -> false
in
s_need_one xs
(The code uses integer constants in patterns, which is an occasionally convenient OCaml feature, but not
essential to the example.)
In terms of syntax, we define mutually recursive functions by simply putting the keyword and between all
the mutually recursive functions. The type-checker will type-check all the functions (two in the example
above) together, allowing calls among them regardless of order.
Here is a second (silly) example that also uses two mutually recursive type bindings. The definition of types
t1 and t2 refer to each other, which is allowed by using and in place of type for the second one. This defines
two new types, t1 and t2.
Now suppose we wanted to implement the “no zeros or empty strings” functionality of the code above but
for some reason we did not want to place the functions next to each other or we were in a language with no
support for mutually recursive functions. We can write almost the same code by having the “later” function
pass itself to a version of the “earlier” function that takes a function as an argument:
let no_zeros_or_empty_strings_t1 f x =
match x with
| Foo i -> i <> 0
| Bar y -> f y
18
Code maintenance: Can you simplify, clean up, or reorganize code without changing how the rest of
the program behaves?
Backward compatibility: Can you add new features without changing how any of the existing features
work?
Optimization: Can you replace code with a faster or more space-efficient implementation?
Abstraction: Can an external client tell if I make this change to my code?
Also notice that our use of restrictive module types in the previous lecture was largely about equivalence:
by using a stricter interface, we make more different implementations equivalent because clients cannot tell
the difference.
We want a precise definition of equivalence so that we can decide whether certain forms of code maintenance
or different implementations of modules are actually okay. We do not want the definition to be so strict that
we cannot make changes to improve code, but we do not want the definition to be so lenient that replacing
one function with an “equivalent” one can lead to our program producing a different answer. Hopefully,
studying the concepts and theory of equivalence will improve the way you look at software written in any
language.
There are many different possible definitions that resolve this strict/lenient tension slightly differently. We
will focus on one that is useful and commonly assumed by people who design and implement programming
languages. We will also simplify the discussion by assuming that we have two implementations of one function
and we want to know if they are equivalent.
The intuition behind our definition is as follows:
A function f is equivalent to a function g (or similarly for other pieces of code) if they produce the
same answer and have the same side-effects no matter where they are called in any program with any
arguments.
Equivalence does not require the same running time, the same use of internal data structures, the same
helper functions, etc. All these things are considered “unobservable”, i.e., implementation details that
do not affect equivalence.
As an example, consider two very different ways of sorting a list. Provided they both produce the same final
answer for all inputs, they can still be equivalent no matter how they worked internally or whether one was
faster. However, if they behave differently for some lists, perhaps for lists that have repeated elements, then
they would not be equivalent.
However, the discussion above was simplified by implicitly assuming the functions always return and have
no other effect besides producing their answer. To be more precise, we need that the two functions when
given the same argument in the same environment:
These requirements are all important for knowing that if we have two equivalent functions, we could replace
one with the other and no use anywhere in the program will behave differently.
19
Another Benefit of Side-Effect-Free Programming
One easy way to make sure two functions have the same side effects (mutating references, doing input/output,
etc.) is to have no side effects at all. This is exactly what functional languages like OCaml encourage. Yes,
in OCaml you could have a function body mutate some global reference or something, but it is generally
bad style. Other functional languages are pure functional languages meaning there really is no way to do
mutation inside (most) functions.
If you “stay functional” by not doing mutation, printing, etc. in function bodies as a matter of policy, then
callers can assume lots of equivalences they cannot otherwise. For example, can we replace (f x)+(f x)
with (f x)*2? In general, that can be a wrong thing to do since calling f might update some counter or
print something. In OCaml, that’s also possible, but far less likely as a matter of style, so we tend to have
more things be equivalent. A more compelling example is replacing [Link] g ([Link] f xs) with
[Link] (compose g f) xs, which is more efficient for long lists. This is equivalent if f and g have no
side-effects, but may not be otherwise. In a purely functional language, we are guaranteed the replacement
does not change anything. The general point is that mutation really gets in your way when you try to decide
if two pieces of code are equivalent — it is a great reason to avoid mutation.
In addition to being able to remove repeated computations (like (f x) above) when maintaining side-effect-
free programs, we can also reorder expressions much more freely. For example, in Java, C, etc.:
int a = f(x);
int b = g(y);
return b - a;
since f and g can get called in a different order. Again, this is possible in OCaml too, but if we avoid
side-effects, it is much less likely to matter. (We might still have to worry about a different exception getting
thrown and other details, however.)
Standard Equivalences
Equivalence is subtle, especially when you are trying to decide if two functions are equivalent without knowing
all the places they may be called. Yet this is common, such as when you are writing a library that unknown
clients may use. We now consider several situations where equivalence is guaranteed in any situation, so
these are good rules of thumb and are good reminders of how functions and closures work.
First, recall the various forms of syntactic sugar we have learned. We can always use or not use syntactic
sugar in a function body and get an equivalent function. If we couldn’t, then the construct we are using is
not actually syntactic sugar. For example, these definitions of f are equivalent no matter what g is bound
to:
let f x = let f x =
if x then x && g x
g x
else
false
20
Notice though, that we could not necessarily replace x && g x with if g x then x else false if g could
have side effects or not terminate.
Second, we can change the name of a local variable (or function parameter) provided we change all uses of
it consistently. For example, these two definitions of f are equivalent:
let y = 14 let y = 14
let f x = x+y+x let f z = z+y+z
But there is one rule: in choosing a new variable name, you cannot choose a variable that the function body
is already using to refer to something else. For example, if we try to replace x with y, we get let y = y+y+y,
which is not the same as the function we started with. A previously-unused variable is never a problem.
Third, we can use or not use a helper function. For example, these two definitions of g are equivalent:
let y = 14 let y = 14
let g z = (z+y+z)+z let f x = x+y+x
let g z = (f z)+z
Again, we must take care not to change the meaning of a variable due to f and g having potentially different
environments. For example, here the definitions of g are not equivalent:
let y = 14 let y = 14
let y = 7 let f x = x+y+x
let g z = (z+y+z)+z let y = 7
let g z = (f z)+z
Fourth, as we have explained before with anonymous functions, unnecessary function wrapping is poor
style because there is a simpler equivalent way. For example, let g y = f y and let g = f are always
equivalent. Yet once again, there is a subtle complication. While this works when we have a variable like f
bound to the function we are calling, in the more general case we might have an expression that evaluates
to a function that we then call. Are let g y = e y and let g = e always the same for any expression e?
No.
As a silly example, consider let h() (print_string "hi" ; fun x -> x+x) and e is h(). Then let g y = (h()) y
is a function that prints every time it is called. But let g = h() is a function that does not print — the
program will print "hi" once when creating the binding for g. This should not be mysterious: we know that
let-bindings evaluate their right-hand sides “immediately” but function bodies are not evaluated until they
are called.
A less silly example might be if h might raise an exception rather than returning a function.
Fifth, it is almost the case that let p = e1 in e2 can be sugar for (fun p -> e2) e1. After all, for any
expressions e1 and e2 and pattern p, both pieces of code:
Evaluate e1 to a value
21
Since the two pieces of code “do” the exact same thing, they must be equivalent. In Racket, this will be
the case (with different syntax). In OCaml, the only difference is the type-checker: The variables in p are
allowed to have polymorphic types in the let-version, but not in the anonymous-function version.
For example, consider let x = (fun y -> y) in (x 0, x true). This silly code type-checks and returns
(0,true) because x has type ’a->’a. But (fun x -> (x 0, x true)) (fun y -> y) does not type-check
because there is no non-polymorphic type we can give to x and function-arguments cannot have polymorphic
types. This is just how type-inference works in OCaml.
22