Skip to content
scrambled
Programming Language Theory Book

Chapter 5. The untyped/pure lambda-calculus

Review the definition and properties of the untyped or pure lambda calculus.

-> The underlying "computational substrate" for most of the type systems.

Context & history ​

  • Mid 1960, Peter Landin observed that a complex programming language can be understood by:

    • The formulation of the language as a tiny core calculus capturing the language's essential mechanisms.

    • Together with a collection of convenient derived forms whose behavior is understood by translating them into the core.

      (Landin, 1964, 1965, 1966; Tennent, 1981)

  • The core language used by Landin was the lambda-calculus, a formal system invented in the 1920s by (Alonzo Church, 1936, 1941), in which all computation is reduced to the basic operations of function definition and application.

  • Following Landin's insight, as well as the pioneering work on Lisp (John McCarthy, 1959, 1981), the lambda-calculus has seen widespread use in many areas.

    • The specification of programming language features.

    • Language design.

    • Language implementation.

    • The study of type systems.

  • The lambda calculus's importance arises from the fact that it can be viewed simultaneously as:

    • A simple programming language in which computations can be described.

    • A mathematical object about which rigorous statements can be proved.

  • The λ-calculus is just one among many core calculi used to study and define programming languages.

    • The π-calculus (Milner, Parrow, Walker, 1991-1992) serves as a core language for modeling message-passing concurrency.

    • Abadi and Cardelli's object calculus (1996) captures the essential features of object-oriented languages.

      -> Most of the ideas and techniques for the λ-calculus transfer quite directly to the other calculi.

  • The λ-calculus can be enriched in several ways.

    • We can add concrete syntax for features like numbers, tuples, records, etc., whose behavior can already be encoded in the core language, but which are more convenient to work with in "built-in" form.

    • We can also add more complex features such as:

      • Mutable reference cells.

      • Nonlocal exception handling.

        These can, in principle, be simulated in the pure λ-calculus, but only via heavy and indirect encodings.

    • Such extensions gradually lead to full-fledged programming languages, for example:

      • ML (Gordon, Milner, Wadsworth, 1979; Milner, Tofte, Harper, 1990; Milner, Tofte, Harper, MacQueen, 1997; etc.)

      • Haskell (Hudak et al., 1992)

      • Scheme (Sussman and Steele, 1975; Kelsey, Clinger, Rees, 1998)

    • Extending the core language almost always goes hand-in-hand with extending the type system: richer language features typically require richer typing disciplines to describe and control their behavior.

Basics ​

  • Procedural (or functional) abstraction is a key feature of essentially all programming languages.

    λn.expr -> a shorthand for "the function that, for each n, yields expr".

    Example: factorial=λn. if n=0 then 0 else n∗factorial (n−1)

    -> factorial(0) means "the function λn. if n=0 then 0 else n∗factorial (n−1) applied to 0".

  • Lambda-calculus (or λ-calculus) consists of:

    • Function definition.

    • Function application.

      …in the purest possible form.

  • In the lambda-calculus, everything is a function:

    • The arguments accepted by functions are themselves functions.

    • The result returned by a function is another function.

  • Lambda-calculus comprises just 3 sorts of terms:

    • A variable x by itself is a term.

    • The abstraction of a variable x from a term t1, written λx.t1, is a term.

    • The application of a term t1 to another term t2, written t1t2, is a term.

The syntax of lambda calculus

Abstract and concrete syntax ​

  • It's useful to distinguish two levels of structure:

    • Concrete syntax (surface syntax): the actual strings of characters that programmers read and write.

    • Abstract syntax: a simpler, internal representation of programs as labeled trees, usually called abstract syntax trees (ASTs).

  • Abstract syntax trees (ASTs) make the structure of programs explicit:

    • They represent programs as trees instead of raw text.

    • This tree structure is ideal for:

      • Formal language definitions and proofs about languages.

      • The internal workings of compilers and interpreters, which need to manipulate program structure precisely.

  • Concrete syntax -> Abstract syntax usually happens in two stages:

    • Lexical analysis (lexing)

      • A lexer converts the raw character stream into a sequence of tokens: identifiers, keywords, constants, punctuation, etc.

      • It also:

        • Removes comments.

        • Handles whitespace and capitalization conventions.

        • Deals with formats for numeric and string literals.

    • Parsing

      • A parser takes the sequence of tokens and builds an abstract syntax tree.

      • During parsing, rules like operator precedence and associativity reduce the need for excessive parentheses in the source code.

      • For example:

        • The operator binds more tightly than +.

        • So the expression 1 + 2 * 3 is parsed as 1 + (2 * 3) rather than (1 + 2) * 3.

  • The main focus is on abstract syntax, not concrete syntax:

    • Grammars (like the grammar for λ-terms) should be read as descriptions of legal tree structures, not just valid strings of characters.

    • When we write terms in text-examples, definitions, theorems, proofs-we use a linear concrete notation, but we always implicitly mean the corresponding abstract syntax tree.

  • To avoid writing too many parentheses when λ-terms are written in linear form, we adopt two standard conventions:

    • Application is left-associative

      • The expression s t u is read as (s t) u, not s (t u).

    • Abstraction bodies extend as far to the right as possible

      • The expression λx. λy. x y x is read as:

        • λx. (λy. ((x y) x))

      • That is, the body of each λ-abstraction includes everything to its right unless parentheses say otherwise.

Variables and metavariables ​

  • Metavariables vs object-language variables

    • t, s, u (with subscripts) are metavariables for arbitrary terms.

    • x, y, z are often metavariables for arbitrary variables (i.e., they range over variable names).

  • Name overloading & disambiguation

    • The same symbols x, y, etc. are also used as actual variables in the object language (inside terms).

    • The intended role-metavariable vs. object-language variable-is always clear from context.

  • Example: In "The term λx.λy.xy has the form λz.s, where z=x and s=λy.xy":

    • z and s are metavariables.

    • x and y are object-language variables.

Scope ​

  • In λx.t, λx is a binder and its scope is the body t.

  • An occurrence of x is bound if it appears inside the body t of some enclosing abstraction λx.t.

  • An occurrence of x is free if it is not bound by any surrounding abstraction on x.

  • Examples:

    • xy -> both x and y are free.

    • λy.xy -> y is bound, x is free.

    • λx.x -> x is bound.

    • λz.λx.λy.x(yz) -> all occurrences of x, y, and z are bound.

    • (λx.x)x -> the first x (inside λx.x) is bound, the second x (the argument) is free.

  • A term with no free variables is called closed.

  • Closed terms are also called combinators.

  • Example: the identity combinator.

    • id=λx.x

    • It simply returns its argument unchanged.

Operational semantics ​

  • In its pure form, the lambda-calculus has:

    • No built-in constants or primitive operators.

    • No numbers, arithmetic, conditionals, records, loops, sequencing, or I/O.

    • The only way to compute is by applying functions to arguments (and both are just terms).

  • A single computation step:

    • Rewrite a function application whose left side is an abstraction.

    • Substitute the argument for the bound variable in the body.

      (λx. t12) t2→[x↦t2] t12

      where [x \mapsto t_2] t_1_2 ParseError: Double subscript at position 20: …mapsto t_2] t_1_̲2 means "the term obtained by replacing all free occurrences of x in t_1_2 ParseError: Double subscript at position 4: t_1_̲2 with t2."

  • Examples:

    • (λx. x) y→y

    • (λx. x (λx. x)) (u r)→u r (λx. x)

  • Follow Church:

    • A term of the form (λx. t12) t2 is called a redex (reducible expression).

    • The act of rewriting a redex using the rule above is called beta-reduction (\beta-reduction).

  • Evaluation strategy: Which redex (or redexes) in a term may be reduced at the next evaluation step.

    • There can be multiple evaluation strategies for the lambda calculus.

    • Different strategies (e.g., normal order, call by value) choose different redexes, but all are based on the same beta-reduction rule above.

Evaluation strategies ​

  • Context:

    • id=λx. x.

    • Consider this term: t=id (id (λz. id z)).

      There are 3 redexes: The outer id (...), the inner id (\lambdaz. id z), and the id z inside.

  • There can be many reductions for the full beta-reduction strategy.

  • Full beta-reduction

    • Any redex can be reduced anywhere in the term at any step.

      (λx. t12) t2→[x↦t2] t12

    • One possible full-reduction sequence (reducing innermost first):

      id (id (λz. id z))

      → id (id (λz. z))

      → id (λz. z)

      → λz. z

      ↛

  • Under the later strategies, the one-step evaluation relation is a partial function: each term has at most one next step.

  • Normal order

    • Always reduce the leftmost, outermost redex first.

    • The normal-order reduction:

      id (id (λz. id z))

      → id (λz. id z)

      → λz. id z

      → λz. z

      ↛

  • Call by name

    • Like normal order, but no reduction inside abstractions.

    • Same first steps as normal order, but stop once the result is a \lambda-abstraction:

      id (id (λz. id z))

      → id (λz. id z)

      → λz. id z

      ↛

    • Here, \lambdaz.idz is treated as a normal form (no further reduction inside the body).

    • Variants of call by name appear in languages like Algol 60 and in optimized form (call by need), Haskell.

  • Call by need

    • An optimized non-strict strategy (used in Haskell).

    • Similar to call by name, but shares the result of evaluating an argument:

      • First use of the argument evaluates it.

      • All other uses reuse that value (no re-evaluation).

    • Conceptually works on graphs (shared structure), not just trees.

  • Call by value

    • Only outermost redexes are reduced, and only when the argument is already a value.

    • The example reduces as:

      id (id (λz. id z))

      → id (λz. id z)

      → λz. id z

      ↛

    • This is a strict strategy: function arguments are always evaluated, whether or not they are actually used.

    • In contrast, non-strict (or lazy) strategies like call by name/need only evaluate arguments that are actually used.

  • The choice of evaluation strategy has little effect on the core issues of type system design. Most typing concepts and techniques apply similarly across strategies.

StrategyWhich redex is reduced?Reduce inside \lambda-bodies?When are arguments evaluated?Strict?ProsCons
Full \beta-reduction Any redex, anywhere in the term Yes No fixed policy; any redex may be chosen Not really an execution strategy Very flexible; useful for theoretical reasoning about equivalence and normalization. Non-deterministic; not a realistic implementation strategy; no notion of "order of evaluation."
Normal order Leftmost, outermost redex Yes Arguments reduced only when needed (when their redex is outermost) Non-strict Normalizing: if a normal form exists, this strategy will find it; good for reasoning. Can be inefficient (may re-evaluate the same argument many times); less practical in naive form.
Call by name Leftmost, outermost redex, but not inside \lambda-abstractions No Arguments are substituted but not evaluated inside \lambda-bodies Non-strict Captures lazy behavior at a simple semantic level; good as a theoretical model. Still re-evaluates arguments when used multiple times; not efficient enough by itself.
Call by need Like call by name, but with sharing of evaluated arguments No Argument evaluated at most once, then its value is shared Non-strict (lazy) Avoids repeated work via sharing; basis for lazy functional languages (e.g. Haskell). Requires graph-based implementation with sharing; runtime model more complex than CBV.
Call by value Outermost redex whose argument is already a value Typically no Arguments evaluated before function body runs Strict Simple, efficient, and matches most real-world languages (ML, OCaml, Java, etc.). Cannot evaluate terms that rely on non-termination avoidance (e.g. some lazy constructions); less flexible than lazy strategies.

Programming in the lambda calculus ​

  • The lambda-calculus has an extremely small core (just variables, abstraction, application) but is surprisingly powerful.

  • Many language features (multi-argument functions, booleans, pairs, numbers, lists, etc.) can be encoded inside the pure calculus.

  • Motivations:

    • These encodings are "warm-up exercises" to understand the system.

    • Not endorsements of \lambda-calculus as a practical programming language.

Multiple arguments (currying) ​

  • The pure lambda-calculus has no built-in multi-argument functions.

  • Instead of writing something like f(x,y), we use higher-order functions and currying:

    • Desired informal form: f(x,y)=s[x↦v,y↦w]

    • Curried encoding:

      f=λx. λy. s

      • First apply f to v: f v=λy. [x↦v]s.

      • Then apply to w: f v w=(λy. [x↦v]s) w→[y↦w][x↦v]s .

  • This transformation of multi-argument functions to chains of single-argument functions is called currying (after Haskell Curry).

Church booleans ​

  • Encode booleans as functions:

    • tru=λt. λf. t

    • fls=λt. λf. f

  • A generic "if" (conditional) combinator: test=λl. λm. λn. l m n

    • test tru v w→v

    • test fls v w→w

  • Intuition: the boolean itself acts as the conditional:

    • tru v w chooses the first argument.

    • fls v w chooses the second argument.

Boolean operators ​

  • and=λb. λc. b c fls

    • If b=tru, result is c.

    • If b=fls, result is fls.

    • So and b c is tru iff both b and c are tru.

Exercise 5.2.1 Define logical or and not functions.

Solution
  • or=λb. λc. b tru c

  • not=λb. λt. λf. b f t

Pairs ​

  • Encode a pair of values using booleans:

    pair=λf. λs. λb. b f s

    fst=λp. p tru

    snd=λp. p fls

  • Intuition:

    • pair v w is a function expecting a boolean b and returning b v w.

    • If b=tru, we get v if b=fls, we get w.

    • So:

      • fst (pair v w)→v

      • snd (pair v w)→w

Church numerals ​

  • Encode natural numbers as "iterate a function n times":

    c0=λs. λz. z

    c1=λs. λz. s z

    c2=λs. λz. s (s z)

    c3=λs. λz. s (s (s z))

    …

  • In general, cn represents "apply s to z, n times".

  • Note: c0 and fls are actually the same term, just used with different intended meanings. This is interesting, as 0 is falsy in most programming languages.

  • Looks like fixed point approximation?

  • Successor (n↦n+1): scc=λn. λs. λz. s (n s z)

  • Addition (m+n):

    plus=λm. λn. λs. λz. m s (n s z)

    • First use n s z to apply s n times.

    • Then apply s m more times via m.

  • Multiplication (m×n) using plus:

    times=λm. λn. m (plus n) c0

    • plus n is a function "add n".

    • m (plus n) c0 means "start at 0, add n, m times" \to m×n.

Exercise 5.2.2 Find another way to define the successor function on Church numerals.

Solution

scc=λn. λs. n s (c1 s z)

Exercise 5.2.3 Is it possible to define multiplication on Church numerals without using plus?

Solution

times=λm. λn. λs. λz. m (n s) z

Exercise 5.2.4 Define a term for raising one number to the power of another.

Solution

pow=λm. λn. λs. λz. m (times n) c0

  • Zero test: iszro=λm. m (λx. fls) tru

- iszro c0→tru (no steps applied). - iszro ck→fls for k≥1 (at least one overwrite).

  • Predecessor:

- Idea: track pairs (ci,ci+1). - Helper definitions: - zz=pair c0 c0 - ss=λp. pair (snd p) (succ (snd p))

- Definition: prd=λm. fst (m ss zz) - Starting from (c0,c0), each application of ss transforms (ci,cj)↦(cj, cj+1) - After m steps, we get (cm−1,cm) or (c0,c0) when m=0. - Taking fst gives the predecessor.

Exercise 5.2.5 Use prd to define a subtraction function.

Solution

sub=λm. λn. n prd m

Exercise 5.2.6 Approximately how many steps of evaluation (as a function of n) are required to calculate prd cn?

Solution

TODO

Exercise 5.2.7 Write a function equal that tests two numbers for equality and returns a Church boolean.

Solution

equal=λm. λn. and (iszro (sub m n)) (iszro (sub n m))

Other datatypes ​

Many other structures (lists, trees, variants, arrays) can be encoded similarly.

Exercise 5.2.8 A list can be represented in the lambda-calculus by its fold function. (OCaml's name for this function is fold_left; it is also sometimes called reduce.) For example, the list [x,y,z] becomes a function that takes two arguments c and n and returnsc x (c y (c z n))). What would the representation of nil be? Write a function cons that takes an element h and a list (that is, a fold function) t and returns a similar representation of the list formed by prepending h to t. Write isnil and head functions, each taking a list parameter. Finally, write a tail function for this representation of lists (this is quite a bit harder and requires a trick analogous to the one used to define prd for numbers).

Solution
  • nil=λc. λn. n

  • cons=λh. λt. λc. λn. (c h (t c n))

  • isnil=λt. t (λe. λn. fls) tru

  • head=λt. t tru nil

  • tail=λt. fst (t tt nn)

  • nn=pair nil nil

  • tt=λe. λp. pair (snd p) (cons e (snd p))

Observations ​

The definitions for the Church numerals and lists encoding get me thinking about the technique:

  • Think of a base value in the domain: 0 for numbers and nil for lists.

  • Think of a base operation in the domain that can create any values from the base value: addition for numbers and cons-ing for lists.

  • Then:

    • For numerals:

      • The 0-th value: λc. λz. z.

      • The n-th value: λc. λz. c (vn−1 c z).

    • For lists:

      • The 0-th value: λc. λn. n.

      • The 1-st value: λc. λn. c e1 (v0 c n) .

      • The n-th value: λc. λn. c en (vn−1 c n).

Enriching the calculus ​

  • In the pure lambda-calculus we can already encode booleans, numbers, and their operations, so in principle we can write all our programs there.

  • In practice, it is often more convenient to work in an enriched language that has primitive booleans and numbers as well.

  • Pure lambda-calculus is denoted by λ.

    No built-in booleans or numbers; everything is encoded.

  • Pure lambda-calculus enriched with booleans and naturals is denoted by λ𝐍𝐁.

    Has primitive true, false, numeric literals, succ, pred, iszero, etc. (as in the earlier arithmetic language in Chapter 3. Untyped arithmetic expressions).

  • In λ𝐍𝐁, there are effectively two versions of booleans and numbers:

    • Real (primitive) values

    • Encoded (Church) values

      Conversions:

    • Church boolean \to primitive boolean: realbool=λb. b true false.

    • Primitive boolean \to Church boolean: churchbool=λb. if b then tru else fls.

Why primitive values help: evaluation order & call by value ​

  • We use call by value (CBV):

    • Do not reduce under lambdas.

    • Only reduce a redex when its argument is already a value.

  • Example: successor of a Church numeral

    • We expect: scc c1→c2

    • But in CBV we actually get a \lambda-term that is extensionally equal to c2, but not syntactically the same; some computation is "stuck under a \lambda" and cannot be reduced further under CBV.

    • So scc c1 is behaviorally equivalent to c2 (they act the same when given s and z), but not literally the same normal form.

  • Example: multiplication

    • times c2 c2 does not reduce to c4 under CBV; it reduces to a large lambda-term with a lot of latent computation.

    • We can still check behavior by:

      • Comparing at the Church level:

        equal c4 (times c2 c2)→tru

      • Or more conveniently, converting to a primitive number:

        realnat (times c2 c2)→4

    • Applying realnat "finishes" the computation, because it supplies the missing arguments (succ and 0) and forces all remaining \beta-reductions.

Therefore:

  • Encodings show that pure λ is expressive enough.

  • Primitives + conversions in λ𝐍𝐁 make examples and reasoning under call-by-value much easier to see and check.

Recursion ​

  • Some terms never reach a normal form; they are said to diverge.

  • The classic divergent/big omega combinator:

    Ω=(λx. x x) (λx. x x)
    • This term has exactly one redex; reducing it just gives back the same term ω again, so evaluation loops forever.

    • Any term with no normal form (like Ω) is said to diverge.

  • The omega combinator has a useful generalization called the fixed-point combinator.

    • The call-by-name version is called the Y-combinator.

    • The call-by-value version is called the Z-combinator.

  • The fixed-point combinator fix is used to specify recursive definitions in the untyped \lambda-calculus:

    fix=λf. (λx. f (λy. x x y)) (λx. f (λy. x x y))
    • fix f gives you a term h such that, operationally,

      fix f≈ f (fix f)

      so f receives a copy of its own result as an argument.

    • How to derive the call-by-value fixed-point combinator: (Friedman and Felleisen, 1996, Chapter 9).

  • There is also a simpler call-by-name fixed-point combinator:

    Y=λf. (λx. f (x x)) (λx. f (x x))

    but:

    • Y diverges under call-by-value.

    • This is because CBV tries to evaluate x x too eagerly.

      -> It is not usable as-is in the CBV setting.

  • Using fix to define recursive functions

    We want recursive definitions of the form:

    • Informal recursive style:

      h=⟨body containing h⟩
    • To encode this in \lambda-calculus:

      1. First define a non-recursive generator:

        g=λf. ⟨body containing f⟩
      2. Then define the recursive function as:

        h=fix g
    • Operational idea:

      • fix g expands to something like g (fix g).

      • Everywhere f appears in the body of g, it is effectively unrolled with another copy of the recursive function.

        -> Each recursive call "unrolls" one more copy of the body.

  • Example: factorial with Church numerals

    g=λfct. λn. if realeq n c0  then c1  else times n (fct (prd n))factorial=fix g
    • The fix machinery ensures that each time fct is applied.

      -> fct effectively behaves like factorial itself.

      -> The definition is unrolled one step at a time.

    • Conceptually, fct is a self-replicator:

      Applying fct n feeds fct and n back into g.

      -> Producing another expanded copy of the recursive body, with new fcts ready to continue the process.

Exercise 5.2.9 Why did we use a primitive if in the definition of g, instead of the Church-boolean test function on Church booleans? Show how to define the factorial function in terms of test rather than if.

Solution
  • This is because if doesn't need to evaluate both of its branches before evaluating itself, while test needs to evaluate both of its branches first.

  • In other words, if is lazy and test is eager.

  • If we use test in the same way as if in the above example, it would yield divergent terms on every application.

  • How to define factorial in terms of test: Simulate call-by-name using thunks.

factorial n=λfct. λn. (test (equal n 0) (λ_. c1) (λ_. (times n (fct (sub n 1))))) tru

Exercise 5.2.10 Define a function churchnat that converts a primitive natural number into the corresponding Church numeral.

Solution

churchnat=fix λg. λn. if n=0 then c0 else succ (g (n−1))

Exercise 5.2.11 Use fix and the encoding of lists from Exercise 5.2.8 to write a function that sums lists of Church numerals.

Solution

sum=fix λg. λl. if isnil l then 0 else sum (head l) (g (tail l))

Representation ​

  • What does it mean that Church numerals "represent" ordinary numbers?

  • Ordinary naturals (as in λ𝐍𝐁):

    • A constant: 0.

    • Operations:

      • succ: numbers → numbers.

      • pred: numbers → numbers.

      • iszero: numbers → booleans.

    • Their behavior is fixed by evaluation rules (e.g. succ 2=3, iszero 0=true).

  • Church encoding idea: represent all of these as lambda-terms.

    • Zero: c0=λs. λz. z

      • Other behaviorally equivalent (non-canonical) terms (e.g. \lambdas. \lambdaz. (\lambdax. x) z) also count as representations of 0.

    • Successor/predecessor:

      • scc represents succ: if t represents n, then scc t evaluates to a representation of n+1.

      • prd represents pred: if t represents n, then prd t evaluates to a representation of max⁡(n−1,0).

    • Zero test: iszro represents iszero:

      • If t represents 0, iszro t evaluates to true.

      • If t represents any n≠0, iszro t evaluates to false.

  • Representation correctness (observational view):

    • Take any program that:

      • Uses primitive numbers and operations (0, succ, pred, iszero, \ldots)

      • Produces a boolean result.

    • Replace all numbers and arithmetic operations with their Church encodings (c0, scc, prd, iszro, \ldots).

    • After evaluation, the final boolean result is the same.

    • Therefore, no observable difference: Church numerals and primitive naturals behave the same from the program's point of view.

Formalities ​

Syntax ​

  • The usual \lambda-calculus grammar (e.g. t::=x | \lambdax.t | t t) is shorthand for an inductively defined set of abstract syntax trees.

  • Terms:

    • Fix a countable set of variable names V.

    • The set of terms T is the smallest set such that:

      1. x∈T for every x∈V.

      2. if t1∈T and x∈V, then λx. t1∈T.

      3. if t1∈T and t2∈T, then t1 t2∈T.

  • Free variables (FV)

    • FV(t) = set of variables that occur free in term t.

    • Rules:

      1. FV(x)=x.

      2. FV(λx. t1)=FV(t1)∖x.

      3. FV(t1 t2)=FV(t1)∪FV(t2).

Exercise 5.3.3 Give a careful proof that |FV(t)|≤size(t) for every term t.

Solution

TODO

Substitution ​

  • Throughout, two definitions of substitution are used:

    • The compact and intuitive definition shown before: [x→s], optimized for examples and in mathematical definitions and proofs.

    • Another developed in Chapter 6, is notationally heavier, depending on an alternative "de Bruijn presentation" of terms in which named variables are replaced by numeric indices, but is more convenient for the concrete ML implementations.

  • Goal: define substitution [x↦s]t in the \lambda-calculus correctly (capture-avoiding).

Naive substitution (wrong #1) ​

  • Defined structurally on term t:

    • [x↦s]x=s.

    • [x↦s]y=yif x≠y

    • [x↦s](λy. t1)=λy. [x↦s] t1

    • [x↦s](t1 t2)=([x↦s] t1) ([x↦s] t2)

  • Problem: does not distinguish free vs bound occurrences of x.

    Example: [x↦y](λx. x)=λx. y

    -> This conflicts with the basic intuition about functional abstractions that the names of bound variables do not matter: The identity function is exactly the same whether we write it \lambdax. x or \lambday. y or λfranz. franz. If these do not behave exactly the same under substitution, then they will not behave the same under reduction either, which seems wrong.

  • Mistake: No distinction between free occurrences of a variable (which should get replaced during substitution) and bound ones, which should not.

Improved substitution (wrong #2) - stop at binder with the same name ​

  • Modify abstraction case to not substitute under a binder with the same name:

    • [x↦s]x=s

    • [x↦s]y=yif y≠x

    • [x↦s](λy. t1)={λy. t1if y=xλy. [x↦s] t1if y≠x
    • [x↦s](t1 t2)=([x↦s] t1) ([x↦s] t2)

  • Fixes earlier issue, but introduces variable capture:

    Example: [x↦z](λz. x)=λz. z

Variable capture & capture-avoiding substitution ​

  • Variable capture: a free variable in s becomes bound after substitution into t.

  • To avoid this, in the abstraction case we must ensure the bound variable y:

    • is not x, and

    • does not occur free in s.

  • Capture-avoiding substitution:

    • [x↦s]x=s

    • [x↦s]y=yif y≠x

    • [x↦s](λy. t1)={λy. t1if y=xλy. [x↦s] t1if y≠x and y∉FV(s)
    • [x↦s](t1 t2)=([x↦s] t1) ([x↦s] t2)

  • The above definition is partial: if y≠x but y∈FV(s), no clause applies.

    -> Work with terms "up to renaming bound variables".

Alpha-conversion ​

  • \alpha-conversion: Consistent renaming of a bound variable - \lambday. x y≡\lambdaw. x w.

  • Convention: Terms that differ only by renaming bound variables are interchangeable - We work "up to \alpha-conversion".

  • If substitution would be undefined because y∈FV(s), we rename the bound variable first.

    Example:

    [x↦y z](λy. x y)

    • First rename: \lambday. x y \to \lambdaw. x w

    • Then: [x↦y z](λw. x w)=λw. (y z w)

Final capture-avoiding substitution definition ​

  • Using the \alpha-conversion convention, we assume the binder y is always chosen fresh (\neq x and not free in s), so we can drop the special case:

    • [x↦s]x=s

    • [x↦s]y=yif y≠x

    • [x↦s](λy. t1)=λy. [x↦s] t1if y≠x and y∉FV(s)

    • [x↦s](t1 t2)=([x↦s] t1) ([x↦s] t2)

  • This is the standard capture-avoiding substitution used in \lambda-calculus proofs and definitions.

Operational semantics (lambda calculus) ​

The untyped lambda calculus \lambda

  • Values are exactly lambda-abstractions:

    • v::=λx. t

    • Evaluation stops when it reaches a λ; arbitrary λ-terms can be values.

  • Small-step rules (call-by-value application)

    1. E-AppAbs - computation rule

    2. E-App1 ****(reduce the function part first) - congruence rule

    3. E-App2 (then reduce the argument) - congruence rule

  • How metavariables enforce evaluation order

    • v2 in E-AppAbs: must be a value, so \beta-reduction only fires when the argument is fully evaluated.

    • t1 in E-App1: any term; we reduce the function position first while it can step.

    • v in E-App2: the left side must already be a value, so we only start reducing the argument after the function is done.

    • Combined, these rules enforce call-by-value, left-to-right:

      1. Reduce t1 to a value (E-App1).

      2. Reduce t2 to a value (E-App2).

      3. Apply \beta (E-AppAbs).

  • Special property of the pure \lambda-calculus

    • Since only \lambda-abstractions are values, once t1 has been reduced to a value (by E-App1), it must be a \lambdax. t.

    • This breaks once we enrich the language (e.g. add primitive booleans, numbers), where values include more forms than just \lambda-abstractions.

Exercise 5.3.6 Adapt these rules to describe the other three strategies for evaluation-full beta-reduction, normal-order, and lazy evaluation.

Solution
  • Full beta-reduction

    1. t1→t1′t1 t2→t1′ t2

    2. t2→t2′t1 t2→t1 t2′

    3. t→t′λx. t→λx. t′

    4. (λx. t1) t2→[x→t2]t1

  • Normal-order

    1. (λx. t1) t2→[x↦t2]t1

    2. t1→t1′t1 t2→t1′ t2(t1 is not a λ)

    3. t2→t2′t1 t2→t1 t2′(t1 in normal form)

    4. t→t′λx. t→λx. t′

  • Call-by-name

    1. t1→t1′t1 t2→t1′ t2

    2. (λx. t1) t2→[x→t2]t1

Exercise 5.3.7 Exercise 3.5.16 gave an alternative presentation of the operational semantics of booleans and arithmetic expressions in which stuck terms are defined to evaluate to a special constant wrong. Extend this semantics to λ𝐍𝐁.

Solution

TODO

Exercise 5.3.8 Exercise 4.2.2 introduced a "big-step" style of evaluation for arithmetic expressions, where the basic evaluation relation is "term t evaluates to final result v." Show how to formulate the evaluation rules for lambda-terms in the big-step style.

Solution
  1. t1⇓λx. t1′t2⇓v2[x→v2]t1′⇓vt1 t2⇓v

  2. [x→t2]t1⇓v(λx. t1) t2⇓v