sml chapter9
sml chapter9
This chapter brings together all the concepts we have learned so far. For an ex-
tended example, it presents a collection of modules to implement the λ-calculus
as a primitive functional programming language. Terms of the λ-calculus can be
parsed, evaluated and the result displayed. It is hardly a practical language. Triv-
ial arithmetic calculations employ unary notation and take minutes. However,
its implementation involves many fundamental techniques: parsing, represent-
ing bound variables and reducing expressions to normal form. These techniques
can be applied to theorem proving and computer algebra.
Chapter outline
We consider parsing and two interpreters for λ-terms, with an overview
of the λ-calculus. The chapter contains the following sections:
A functional parser. An ML functor implements top-down recursive descent
parsing. Parsers can be combined using infix operators that resemble the sym-
bols for combining grammatical phrases.
Introducing the λ-calculus. Terms of this calculus can express functional
programs. They can be evaluated using either the call-by-value or the call-by-
name mechanism. Substitution must be performed carefully, avoiding variable
name clashes.
Representing λ-terms in ML. Substitution, parsing and pretty printing are
implemented as ML structures.
The λ-calculus as a programming language. Typical data structures of func-
tional languages, including infinite lists, are encoded in the λ-calculus. The
evaluation of recursive functions is demonstrated.
A functional parser
Before discussing the λ-calculus, let us consider how to write scanners
and parsers in a functional style. The parser described below complements the
pretty printer of the previous chapter. Using these tools, ML programs can read
and write λ-terms, ML types and logical formulæ.
363
364 9 Writing Interpreters for the λ-Calculus
A token is either an identifier or a keyword. This simple scanner does not rec-
ognize numbers. Calling scan performs lexical analysis on a string and returns
the resulting list of tokens.
Before we can parse a language, we must specify its vocabulary. To classify
tokens as identifiers or keywords, the scanner must be supplied with an instance
of the signature KEYWORD:
signature KEYWORD =
sig
val alphas : string list
and symbols : string list
end;
The list alphas defines the alphanumeric keywords like "if" and "let",
while symbols lists symbolic keywords like "(" and ")". The two kinds of
keywords are treated differently:
fun alphaTok a =
if member (a, Keyword .alphas) then Key(a) else Id (a);
Functor Lexical (Figure 9.1 on the preceding page) implements the scanner
using several Substring functions: getc, splitl , string, dropl and all . The
function getc splits a substring into its first character, paired with the rest of
the substring; if the substring is empty, the result is NONE. In Section 8.9 we
met the functions all and string, which convert between strings and substrings.
We also met splitl , which scans a substring from the left, splitting it into two
parts. The function dropl is similar but returns only the second part of the sub-
string; the scanner uses it to ignore spaces and other non-printing characters.
The library predicate Char .isAlphaNum recognizes letters and digits, while
Char .isGraph recognizes all printing characters and Char .isPunct recog-
nizes punctuation symbols.
The code is straightforward and efficient, as fast as the obvious imperative
implementation. The Substring functions yield functional behaviour, but they
work by incrementing indices. This is better than processing lists of characters.
The functor declares function member for internal use. It does not depend
upon the infix mem declared in Chapter 3, or on any other top level functions
not belonging to the standard library. The membership test is specific to type
string because polymorphic equality can be slow.
The lexical analyser is implemented as a functor because the information in
signature KEYWORD is static. We only need to change the list of keywords or
special symbols when parsing a new language. Applying the functor to some
instance of KEYWORD packages that information into the resulting structure.
We could have implemented the lexer as a curried function taking similar infor-
mation as a record, but this would complicate the lexer’s type in exchange for
needless flexibility.
Exercise 9.1 Modify the scanner to recognize decimal numerals in the input.
Let a new constructor Num : integer → token return the value of a scanned
integer constant.
Exercise 9.2 Modify the scanner to ignore comments. The comment brackets,
such as "(*" and "*)", should be supplied as additional components of the
structure Keyword .
makes the parser run forever! Compiler texts advise on coping with these limi-
tations.
Outline of the approach. Suppose that the grammar includes a certain class of
phrases whose meanings can be represented by values of type τ . A parser for
such phrases must be a function of type
The first two of these reject their input unless it begins with the required token,
while empty always succeeds.
Alternative phrases. If ph1 and ph2 have type τ phrase then so does ph1||ph2.
The parser ph1||ph2 accepts all the phrases that are accepted by either of the
parsers ph1 or ph2. This parser, when supplied with a list of tokens, passes them
to ph1 and returns the result if successful. If ph1 rejects the tokens then ph2 is
attempted.
The parser !!ph is the same as ph, except that if ph rejects the tokens then the
entire parse fails with an error message. This prevents an enclosing || operator
from attempting to parse the phrase in another way. The operator !! is typically
used in phrases that start with a distinguishing keyword, and therefore have no
alternative parse; see $-- below.
Modifying the meaning. The parser ph>>f accepts the same inputs as ph, but
returns (f (x ), toks) when ph returns (x , toks). Thus, it assigns the meaning
f (x ) when ph assigns the meaning x . If ph has type σ phrase and f has
type σ → τ then ph>>f has type τ phrase.
The precedences of the infix operators are --, >>, || from highest to lowest.
The body of repeat consists of two parsers joined by ||, resembling the obvious
grammatical definition: a repetition of ph is either a ph followed by a repetition
of ph, or is empty.
The parser ph -- repeat ph returns ((x , xs), toks), where xs is a list. The
operator >> applies a list ‘cons’ (the operator ::), converting the pair (x , xs)
to x :: xs. In the second line, empty yields [] as the meaning of the empty
phrase. In short, repeat ph constructs the list of the meanings of the repeated
phrases. If ph has type τ phrase then repeat ph has type (τ list) phrase.
Beware of infinite recursion. Can the declaration of repeat be simplified by
omitting toks from both sides? No — calling repeat ph would immediately
produce a recursive call to repeat ph, resulting in disaster:
Mentioning the formal parameter toks is a device to delay evaluation of the body of
repeat until it is given a token list; the inner repeat ph is normally given a shorter token
list and therefore terminates. Lazy evaluation would eliminate the need for this device.
These directives have global effect because they are made at top level. We
should also open the structure containing the parsing operators: compound
names cannot be used as infix operators.
Functor Parsing (Figure 9.3 on page 371) implements the parser. The functor
declaration has the primitive form that takes exactly one argument structure, in
this case Lex . Its result signature is PARSE (Figure 9.2).
You may notice that many of the types in this signature differ from those given
in the previous section. The type abbreviation
signature PARSE =
sig
exception SyntaxErr of string
type token
val id : token list -> string * token list
val $ : string -> token list -> string * token list
val empty : 0 a -> 0 b list * 0 a
val || : (0 a -> 0 b) * (0 a -> 0 b) -> 0 a -> 0 b
val !! : (0 a -> 0 b * 0 c) -> 0 a -> 0 b * 0 c
val -- : (0 a -> 0 b * 0 c) * (0 c -> 0 d * 0 e) -> 0 a -> (0 b * 0 d ) * 0 e
val $-- : string * (token list -> 0 a * 0 b) -> token list -> 0 a * 0 b
val >> : (0 a -> 0 b * 0 c) * (0 b -> 0 d ) -> 0 a -> 0 d * 0 c
val repeat : (0 a -> 0 b * 0 a) -> 0 a -> 0 b list * 0 a
val infixes :
(token list -> 0 a * token list) * (string -> int) *
(string -> 0 a -> 0 a -> 0 a) -> token list -> 0 a * token list
val reader : (token list -> 0 a * 0 b list) -> string -> 0 a
end;
is not used; more importantly, some of the types in the signature are more gen-
eral than is necessary for parsing. They are not restricted to token lists.
ML often assigns a function a type that is more polymorphic than we ex-
pect. If we specify the signature prior to coding the functor — which is a dis-
ciplined style of software development — then any additional polymorphism is
lost. When designing a signature, it is sometimes useful to consult the ML top
level and note what types it suggests.
Signature PARSE specifies the type token in order to specify the types of id
and other items. Accordingly, Parsing declares the type token to be equivalent
to Lex .token.
The function reader packages a parser for outside use. Calling reader ph a
scans the string a into tokens and supplies them to the parsing function ph. If
there are no tokens left then reader returns the meaning of the phrase; otherwise
it signals a syntax error.
Parsing infix operators. The function infixes constructs a parser for infix oper-
ators, when supplied with the following arguments:
ph ⊕ ph ⊗ ph ph ph
and groups the atomic phrases according to the precedences of the operators. It
employs the mutually recursive functions over and next.
Calling over k parses a series of phrases, separated by operators of prece-
dence k or above. In next k (x , toks) the argument x is the meaning of the
preceding phrase and k is the governing precedence. The call does nothing un-
less the next token is an operator a of precedence k or above; in this case, tokens
are recursively parsed by over (prec of a) and their result combined with x . The
result and the remaining tokens are then parsed under the original precedence k .
The algorithm does not handle parentheses; this should be done by ph. Sec-
tion 10.6 demonstrates the use of infixes.
Exercise 9.3 Give an example of a parser ph such that, for all inputs, ph ter-
minates successfully but repeat ph runs forever.
Exercise 9.4 A parse tree is a tree representing the structure of a parsed token
list. Each node stands for a phrase, with branches to its constituent symbols
and sub-phrases. Modify our parsing method so that it constructs parse trees.
Declare a suitable type partree of parse trees such that each parsing function
can have type
token list → partree × token list.
Code the operators ||, --, id , $, empty and repeat; note that >> no longer
serves any purpose.
Exercise 9.6 Code the parsing method in a procedural style, where each pars-
ing ‘function’ has type unit → α and updates a reference to a token list by
removing tokens from it. Does the procedural approach have any drawbacks, or
is it superior to the functional approach?
Exercise 9.8 When an expression contains several infix operators of the same
precedence, does infixes associate them to the left or to the right? Modify this
function to give the opposite association. Describe an algorithm to handle a
mixture of left and right-associating operators.
Types such as int, bool list and (α list) → (β list) consist of a type con-
structor applied to zero or more type arguments. Here the type constructor int
is applied to zero arguments; list is applied to the type bool ; and → is applied
to the types α list and β list. ML adopts a postfix syntax for most type construc-
tors, but → has an infix syntax. Internally, such types can be represented by a
string paired with a list of types.
A type can consist merely of a type variable, which can be represented by a
string. Our structure for types has the following signature:
signature TYPE =
sig
datatype t = Con of string * t list | Var of string
val pr : t -> unit
val read : string -> t
end;
• The datatype t comprises the two forms of type, with Con for type
constructors and Var for type variables.
• Calling pr ty prints the type ty at the terminal.
• The function read converts a string to a type.
We could implement this signature using a functor whose formal parameter list
would contain the signatures PARSE and PRETTY . But it is generally simpler
to avoid writing functors unless they will be applied more than once. Let us
therefore create structures for the lexical analyser and the parser, for parsing
types. They will be used later to implement the λ-calculus.
Structure LamKey defines the necessary symbols. Structure LamLex pro-
vides lexical analysis for types and the λ-calculus, while LamParsing provides
parsing operators.
structure LamKey =
struct val alphas = []
and symbols = ["(", ")", "’", "->"]
end;
structure LamLex = Lexical (LamKey);
structure LamParsing = Parsing (LamLex );
Structure Type (Figure 9.4) matches signature TYPE. For simplicity, it only
treats the → symbol; the other type constructors are left as an exercise. The
grammar defines types and atomic types in mutual recursion. An atomic type is
9.4 Example: parsing and displaying types 375
Atom = ’ Id
| ( Type )
This grammar treats → as an infix operator that associates to the right. It inter-
prets ’a->’b->’c as ’a->(’b->’c) rather than (’a->’b)->’c because
’a -> ’b is not an Atom.
The structure contains two local declarations, one for parsing and one for
pretty printing. Each declares mutually recursive functions typ and atom corre-
sponding to the grammar. Opening structure LamParsing makes its operations
available at top level; recall that the infix directives were global.
Parsing of types. Using the top-down parsing operators, the function definitions
in the parser are practically identical to the grammar rules. The operator >>,
which applies a function to a parser’s result, appears three times. Function typ
uses >> to apply makeFun to the result of the first grammar rule, combining
two types to form a function type. Using $-- in the rule prevents the arrow
symbol from being returned as a constituent of the phrase.
Both cases of atom involve >>, with two mysterious functions. During pars-
ing of the type variable ’a, in the first case, >> applies Var o opˆ to the pair
("’", "a"). This function consists of Var composed with string concatenation;
it concatenates the strings to "’a" and returns the type Var "’a".
In the second case of atom, parsing the phrase ( Type ) calls the function
#1, which selects the first component of its argument. Here it takes the pair
(ty, ")") and yields ty. Had we not used $-- to parse the left parenthesis, we
should have needed the even more mysterious function (#2 o #1).
The parsing functions mention the argument toks to avoid looping (like repeat
above) and because a fun declaration must mention an argument.
Pretty printing of types. The same mutual recursion works for displaying as for
parsing. Functions typ and atom both convert a type into a symbolic expression
for the pretty printer, but atom encloses its result in parentheses unless it is just
an identifier. Parentheses appear only when necessary; too many parentheses
are confusing.
The functions blo, str and brk of structure Pretty are used in typical fashion
to describe blocks, strings and breaks. Function atom calls blo with an inden-
9.4 Example: parsing and displaying types 377
tation of one to align subsequent breaks past the left parenthesis. Function typ
calls blo with an indentation of zero, since it includes no parentheses; after the
string " ->", it calls brk 1 to make a space or a line break.
The function pr writes to the terminal (stream TextIO.stdOut), with a right
margin of fifty.
Trying some examples. We can enter types, note their internal representations
(as values of Type.t) after parsing, and check that they are displayed correctly:
Type.read "’a->’b->’c";
> Con ("->", [Var "’a",
> Con ("->", [Var "’b", Var "’c"])])
> : Type.t
Type.pr it;
> ’a -> ’b -> ’c
Type.read "(’a->’b)->’c";
> Con ("->", [Con ("->", [Var "’a", Var "’b"]),
> Var "’c"])
> : Type.t
Type.pr it;
> (’a -> ’b) -> ’c
Our parsing of types is naı̈ve. A string of the form ( Type ) must be parsed
twice. The first grammar rule for Type fails: there is no -> token after the right
parenthesis. The second grammar rule parses it successfully as an Atom. We
could modify the grammar to remove the repeated occurrence of Atom.
Aho et al. (1986) describes lexical analysis and parsing extremely well. It covers both
the top-down approach implemented here using functions, and the bottom-up approach
that underlies ML-Yacc.
Exercise 9.9 Implement parsing and pretty printing of arbitrary type construc-
tors. First, define a grammar for ML’s postfix syntax, as in the examples
Parentheses are optional when a type constructor is applied to one argument not
involving the arrow; thus ’a -> ’b list stands for ’a -> ((’b) list)
rather than (’a -> ’b) list.
forms:
x a variable
(λx .t) functional abstraction
(t u) function application
λ-conversions. These are rules for transforming a λ-term while preserving its
intuitive meaning. Conversions should not be confused with equations such
as x + y = y + x , which are statements about known arithmetic operations.
The λ-calculus is not concerned with previously existing mathematical objects.
The λ-terms themselves are the objects, and the λ-conversions are symbolic
transformations upon them.
Most important is β-conversion, which transforms a function application by
substituting the argument into the body:
Many λ-terms have no normal form. For instance, (λx .x x )(λx .x x ) reduces to
itself by β-conversion. Any attempt to normalize this term must fail to termi-
nate:
(λx .x x )(λx .x x ) ⇒ (λx .x x )(λx .x x ) ⇒ · · ·
A term t can have a normal form even though certain reduction sequences never
terminate. Typically, t contains a subterm u that has no normal form, but u can
be erased by a reduction step. For example, the reduction sequence
(λy.a)((λx .x x )(λx .x x )) ⇒ a
reaches normal form directly, erasing the term (λx .x x )(λx .x x ). This corre-
sponds to a call-by-name treatment of functions: the argument is not evaluated
but simply substituted into the body of the function. Attempting to normalize
the argument generates a nonterminating reduction sequence:
Evaluating the argument prior to substitution into the function body corresponds
to a call-by-value treatment of function application. In this example, the call-
by-value strategy never reaches the normal form. The reduction strategy corre-
sponding to call-by-name evaluation always reaches a normal form if one exists.
You may well ask, in what sense is λx .x x a function? It can be applied to
any object and applies that object to itself! In classical mathematics, a function
can only be defined over some previously existing set of values. The λ-calculus
does not deal with functions as they are classically understood.1
The β-conversion of (λx y.y x )y to λy.y y is incorrect because the free vari-
able y has become bound. The substitution has captured this free variable. By
first renaming the bound variable y to z , the reduction can be performed safely:
(λx z .z x )y b ⇒ (λz .z y)b ⇒ b y
In general, the substitution t[u/x ] will not capture any variables provided no
free variable of u is bound in t.
If bound variables are represented literally, then substitution must sometimes
rename bound variables of t to avoid capturing free variables. Renaming is
complicated and can be inefficient. It is essential that the new names do not
appear elsewhere in the term. Preferably they should be similar to the names
that they replace; nobody wants to see a variable called G6620094.
Abstraction. Suppose that t is a λ-term that we would like to abstract over all
free occurrences of the variable x , constructing the abstraction λx .t. Take, for
instance, x (λy.a x y), which in the name-free notation is
x (λ.a x 0).
To bind all occurrences of x , we must replace them by the correct indices, here 0
and 1, and insert a λ symbol:
λ.0(λ.a 1 0)
This can be performed by a recursive function on terms that keeps count of the
nesting depth of abstractions. Each occurrence of x is replaced by an index
equal to its depth.
before substitution into t; this ensures that they refer to the same abstractions
afterwards.
For instance, in
λz .(λx .x (λy.x ))(a z ) ⇒β λz .a z (λy.a z ),
the argument a z is substituted in two places, one of which lies in the scope
of λy. In the name-free approach, a z receives two different representations:
λ.(λ.0(λ.1))(a 0) ⇒β λ.a 0(λ.a 1)
Exercise 9.11 Show all the reduction sequences for normalizing the term
(λf .f (f a))((λx .x x )((λy.y)(λy.y))).
Exercise 9.12 For each term, show its normal form or demonstrate that it has
none:
(λf x y.f x y)(λu v .u)
(λx .f (x x ))(λx .f (x x ))
(λx y.y x )(λx .f (f x ))(λx .f (f (f (f x ))))
(λx .x x )(λx .x )
Representing λ-terms in ML
Implementing the λ-calculus in ML is straightforward under the name-
free representation. The following sections present ML programs for abstraction
and substitution, and for parsing and pretty printing λ-terms.
We shall need StringDict, the dictionary structure declared in Section 7.10.
It allows us to associate any information, in this case λ-terms, with strings. We
can evaluate λ-terms with respect to an environment of defined identifiers.
9.7 The fundamental operations 385
Datatype t comprises free variables (as strings), bound variables (as indices),
abstractions and applications. Each Abs node stores the bound variable name
for use in printing.
Calling abstract i b t converts each occurrence of the free variable b in t to
the index i (or a greater index within nested abstractions). Usually i = 0 and the
result is immediately enclosed in an abstraction to match this index. Recursive
calls over abstractions in t have i > 0.
Calling absList([x1 , . . . , xn ], t) creates the abstraction λx1 . . . xn .t.
Calling applyList(t, [u1 , . . . , un ]) creates the application t u1 . . . un .
Calling subst i u t substitutes u for the bound variable index i in t. Usually
i = 0 and t is the body of an abstraction in the β-conversion (λx .t)u. The
case i > 0 occurs during recursive calls over abstractions in t. All indices
exceeding i are decreased by one to compensate for the removal of that index.
Calling inst env t copies t, replacing all occurrences of variables defined in
env by their definitions. The dictionary env represents an environment, and
inst expands all the definitions in a term. This process is called instantiation.
Definitions may refer to other definitions; instantiation continues until defined
variables no longer occur in the result.
Signature LAMBDA is concrete, revealing all the internal details. Many val-
ues of type t are improper: they do not correspond to real λ-terms because
they contain unmatched bound variable indices. No term has the representation
Bound i , for any i . Moreover, abstract returns improper terms and subst ex-
pects them. An abstract signature for the λ-calculus would provide operations
upon λ-terms themselves, hiding their representation.
Structure Lambda (Figure 9.5) implements the signature. Function shift is
private to the structure because it is called only by subst. Calling shift i d u
386 9 Writing Interpreters for the λ-Calculus
Exercise 9.15 Explain the use of the fold functionals in the declarations of
absList and applyList.
Exercise 9.16 Declare a signature for the λ-calculus that hides its internal rep-
resentation. It should specify predicates to test whether a λ-term is a variable,
an abstraction or an application, and specify functions for abstraction and sub-
stitution. Sketch the design of two structures, employing two different represen-
tations of λ-terms, that would have this signature.
Term = % Id Id ∗ Term
| Atom Atom ∗
Atom = Id
| ( Term )
Note that phrase ∗ stands for zero or more repetitions of phrase. A term consist-
ing of several Atoms in a row, such as a b c d , abbreviates the nested application
(((a b)c)d ). A more natural grammar would define the phrase class Applic:
Applic = Atom
| Applic Atom
Then we could replace the Atom Atom ∗ in Term by Applic. But the second
grammar rule for Applic is left-recursive and would cause our parser to loop.
Eliminating this left recursion in the standard way yields our original grammar.
Structure ParseTerm (Figure 9.6) uses structures Parse and Lambda, con-
taining the parser and the λ-term operations, to satisfy signature PARSE TERM :
388 9 Writing Interpreters for the λ-Calculus
The structure’s only purpose is to parse λ-terms. Its signature specifies just one
component: the function read converts a string to a λ-term. Its implementa-
tion is straightforward, using components absList and applyList of structure
Lambda.
Exercise 9.17 In function makeLambda, why does the argument pattern have
the form it does?
Even with the name-free representation, bound variables may have to be re-
named when a term is displayed. The normal form of (λxy.x )y is shown as
%y’. y, not as %y. y. Function stripAbs and its auxiliary function strip
handle abstractions. Given λx1 . . . xm .t, the bound variables are renamed to dif-
fer from all free variables in t. The new names are substituted into t as free
variables. Thus, all indices are eliminated from a term as it is displayed.
The mutually recursive functions term, applic and atom prepare λ-terms for
pretty printing. A Free variable is displayed literally. A Bound variable index
should never be encountered unless it has no matching Abs node (indicating
that the term is improper). For an Abs node, the bound variables are renamed;
then foldleft joins them into a string, separated by spaces. An Apply node is
displayed using applic, which corresponds to the grammatical phrase Applic
mentioned in the previous section. Finally, atom encloses a term in parentheses
unless it is simply an identifier.
Exercise 9.19 How will the normal form of (λx y.x )(λy.y) be displayed?
Modify DisplayTerm to ensure that, when a term is displayed, no variable name
is bound twice in overlapping scopes.
Exercise 9.20 Terms can be displayed without substituting free variables for
bound variables. Modify DisplayTerm to keep a list of the variables bound in
the abstractions enclosing the current subterm. To display the term Bound i ,
locate the i th name in the list.
390 9 Writing Interpreters for the λ-Calculus
if true t u = t
if false t u = u
Once we have two distinct truth values and the conditional operator, we can
define negation, conjunction and disjunction. Analogously, the ML compiler
may represent true and false by any bit patterns provided the operations behave
properly.
true ≡ λx y.x
false ≡ λx y.y
if ≡ λp x y.p x y
392 9 Writing Interpreters for the λ-Calculus
Ordered pairs. An encoding must specify a function pair (to construct pairs)
and projection functions fst and snd (to select the components of a pair). The
usual encoding is
pair ≡ λx y f .f x y
fst ≡ λp.p true
snd ≡ λp.p false
where true and false are defined as above. These reductions, and the corre-
sponding equations, are easily verified for all t and u:
fst(pair t u) ⇒∗ t
snd (pair t u) ⇒∗ u
The natural numbers. Of the several known encodings of the natural numbers,
Church’s is the most elegant. Underlined numbers 0, 1, . . . , denote the Church
numerals:
0 ≡ λf x .x
1 ≡ λf x .f x
2 ≡ λf x .f (f x )
..
.
n ≡ λf x .f n (x )
Here f n (x ) abbreviates f (· · · (f x ) · · · ).
| {z }
n times
9.10 Data structures in the λ-calculus 393
The function suc computes the successor of a number, and iszero tests whether
a number equals zero:
suc ≡ λn f x .n f (f x )
iszero ≡ λn.n(λx .false)true
suc n ⇒∗ n + 1
iszero 0 ⇒∗ true
iszero(suc n) ⇒∗ false
add ≡ λm n f x .m f (n f x )
mult ≡ λm n f .m(n f )
expt ≡ λm n f x .n m f x
These can be formally verified by induction, and their underlying intuitions are
simple. Each Church numeral n is an operator to apply a function n times. Note
that
add m n f x = f m (f n (x )) = f m +n (x );
and we take the second component. To formalize this, define prefn to con-
struct g. Then define the predecessor function pre and the subtraction func-
394 9 Writing Interpreters for the λ-Calculus
tion sub:
prefn ≡ λf p.pair (f (fst p)) (fst p)
pre ≡ λn f x .snd (n(prefn f )(pair x x ))
sub ≡ λm n.n pre m
For subtraction, sub m n = pre n (m); this computes the nth predecessor of m.
Lists. Lists are encoded using pairing and the booleans. A non-empty list with
head x and tail y is coded as (false, (x , y)). The empty list nil could be coded
as (true, true), but a simpler definition happens to work:
nil ≡ λz .z
cons ≡ λx y.pair false (pair x y)
null ≡ fst
hd ≡ λz .fst(snd z )
tl ≡ λz .snd (snd z )
The essential properties are easy to check for all t and u:
null nil ⇒∗ true
null (cons t u) ⇒∗ false
hd (cons t u) ⇒∗ t
tl (cons t u) ⇒∗ u
A call-by-name evaluation reduces hd (cons t u) to t without evaluating u, and
can process infinite lists.
Exercise 9.23 Define an encoding of the natural numbers that has a simple
predecessor function.
9.11 Recursive definitions in the λ-calculus 395
A few terms, such as (λx .x x )(λx .x x ), lack even a head normal form. Such
terms can be regarded as undefined.
The function headNF computes the head normal form of t1 t2 by recursively
computing headNF t1 and then, if an abstraction results, doing a β-conversion.
The argument t2 is not reduced before substitution; this is call-by-name.2
Function byName normalizes a term by computing its headNF and then nor-
malizing the arguments of the outermost application. This achieves call-by-
name reduction with reasonable efficiency.
Exercise 9.28 Derive a head normal form of inflist, or demonstrate that none
exists.
Exercise 9.29 Describe how byValue and byName would compute the normal
form of fst(pair t u), for arbitrary λ-terms t and u.
This term could do with normalization. We define a function try such that
try evfn reads a term, applies evfn to it, and displays the result. Using call-
by-value, we reduce "2" to a Church numeral:
Let us take the head of the infinite list [MORE , MORE , . . .]:
try Reduce.byName "hd inflist";
> MORE
operations and ordered pairs as primitive. Rather than interpreting the λ-terms,
we can compile them for execution on an abstract machine. For call-by-value
reduction, the SECD machine is suitable. For call-by-name reduction we can
compile λ-terms into combinators and execute them by graph reduction. The
design and implementation of a simple functional language makes a challenging
project.
Further reading. M. J. C. Gordon (1988) describes the λ-calculus from the
perspective of a computer scientist; he discusses ways of representing data and
presents Lisp code for reducing and translating λ-expressions. Barendregt (1984) is the
comprehensive reference on the λ-calculus. Boolos and Jeffrey (1980) introduce the
theory of computability, including Turing machines, register machines and the general
recursive functions.
N. G. de Bruijn (1972) developed the name-free notation for the λ-calculus, and used
it in his AUTOMATH system (Nederpelt et al., 1994). It is also used in Isabelle (Paulson,
1994) and in Hal, the theorem prover of Chapter 10.
Field and Harrison (1988) describe basic combinator reduction. Modern implemen-
tations of lazy evaluation use more sophisticated techniques (Peyton Jones, 1992).