scrambled
Book

Chapter 5 the untyped pure lambda calculus

Modified just now

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\lambda n. \text{expr} -> a shorthand for "the function that, for each nn, yields expr\text{expr}".

    Example: factorial=λn. if n=0 then 0 else nfactorial (n1)\text{factorial} = \lambda n. \text{ if } n = 0 \text{ then }0 \text{ else } n * \text{factorial }(n-1)

    -> factorial(0)\text{factorial}(0) means "the function λn. if n=0 then 0 else nfactorial (n1)\lambda n. \text{ if } n = 0 \text{ then }0 \text{ else } n * \text{factorial }(n-1) applied to 00".

  • 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 xx by itself is a term.
    • The abstraction of a variable xx from a term t1t_1, written λx.t1\lambda x.t_1, is a term.
    • The application of a term t1t_1 to another term t2t_2, written t1t2t_1\, t_2, 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

    • tt, ss, uu (with subscripts) are metavariables for arbitrary terms.
    • xx, yy, zz are often metavariables for arbitrary variables (i.e., they range over variable names).
  • Name overloading & disambiguation

    • The same symbols xx, yy, 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\lambda x.\, \lambda y.\, x\, y has the form λz.s\lambda z.\, s, where z=xz = x and s=λy.xys = \lambda y.\, x\, y":

    • zz and ss are metavariables.
    • xx and yy are object-language variables.

Scope

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

  • An occurrence of xx is bound if it appears inside the body tt of some enclosing abstraction λx.t\lambda x.\, t.

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

  • Examples:

    • xyx\, y -> both xx and yy are free.
    • λy.xy\lambda y.\, x\, y -> yy is bound, xx is free.
    • λx.x\lambda x.\, x -> xx is bound.
    • λz.λx.λy.x(yz)\lambda z.\, \lambda x.\, \lambda y.\, x\, (y\, z) -> all occurrences of xx, yy, and zz are bound.
    • (λx.x)x(\lambda x.\, x)\, x -> the first xx (inside λx.x\lambda x.\, x) is bound, the second xx (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.xid = \lambda 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[xt2] t12 (\lambda x.\ t_{12})\ t_2 \to [x \mapsto t_2]\ t_{12}

      where [x \mapsto t_2] t_1_2 means "the term obtained by replacing all free occurrences of xx in t_1_2 with t2t_2."

  • Examples:

    • (λx. x) yy(\lambda x.\ x)\ y \to y
    • (λx. x (λx. x)) (u r)u r (λx. x)(\lambda x.\ x\ (\lambda x.\ x))\ (u\ r) \to u\ r\ (\lambda x.\ x)
  • Follow Church:

    • A term of the form (λx. t12) t2(\lambda x.\ t_{12})\ t_2 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\text{id} = \lambda x.\ x.

    • Consider this term: t=id (id (λz. id z))t = \text{id}\ (\text{id}\ (\lambda z.\ \text{id}\ z)).

      There are 3 redexes: The outer id (...)\text{id}\ (...), the inner id (\lambdaz. id z)\text{id}\ (\lambdaz.\ \text{id}\ z), and the id z\text{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[xt2] t12(\lambda x.\ t_{12})\ t_2 \to [x \mapsto t_2]\ t_{12}

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

      id (id (λz. id z))\text{id}\ (\text{id}\ (\lambda z.\ \text{id}\ z))

       id (id (λz. z))\to\ \text{id}\ (\text{id}\ (\lambda z.\ z))

       id (λz. z)\to\ \text{id}\ (\lambda z.\ z)

       λz. z\to\ \lambda z.\ z

      ↛\not\to

  • 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))\text{id}\ (\text{id}\ (\lambda z.\ \text{id}\ z))

       id (λz. id z)\to\ \text{id}\ (\lambda z.\ \text{id}\ z)

       λz. id z\to\ \lambda z.\ \text{id}\ z

       λz. z\to\ \lambda z.\ z

      ↛\not \to

  • 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))\text{id}\ (\text{id}\ (\lambda z.\ \text{id}\ z))

       id (λz. id z)\to\ \text{id}\ (\lambda z.\ \text{id}\ z)

       λz. id z\to\ \lambda z.\ \text{id}\ z

      ↛\not \to

    • Here, \lambdaz.idz\lambdaz.\, \text{id}\, z 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))\text{id}\ (\text{id}\ (\lambda z.\ \text{id}\ z))

       id (λz. id z)\to\ \text{id}\ (\lambda z.\ \text{id}\ z)

       λz. id z\to\ \lambda z.\ \text{id}\ z

      ↛\not \to

    • 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-reductionAny redex, anywhere in the termYesNo fixed policy; any redex may be chosenNot really an execution strategyVery flexible; useful for theoretical reasoning about equivalence and normalization.Non-deterministic; not a realistic implementation strategy; no notion of "order of evaluation."
Normal orderLeftmost, outermost redexYesArguments reduced only when needed (when their redex is outermost)Non-strictNormalizing: 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 nameLeftmost, outermost redex, but not inside \lambda-abstractionsNoArguments are substituted but not evaluated inside \lambda-bodiesNon-strictCaptures 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 needLike call by name, but with sharing of evaluated argumentsNoArgument evaluated at most once, then its value is sharedNon-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 valueOutermost redex whose argument is already a valueTypically noArguments evaluated before function body runsStrictSimple, 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)f(x, y), we use higher-order functions and currying:

    • Desired informal form: f(x,y)=s[xv,yw]f(x, y) = s[x \mapsto v, y \mapsto w]

    • Curried encoding:

      f=λx. λy. sf = \lambda x.\ \lambda y.\ s

      • First apply ff to vv: f v=λy. [xv]sf\ v = \lambda y.\ [x \mapsto v] s.
      • Then apply to ww: f v w=(λy. [xv]s) w[yw][xv]sf\ v\ w = (\lambda y.\ [x \mapsto v] s)\ w \to [y \mapsto w][x \mapsto 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\text{tru} = \lambda t.\ \lambda f.\ t
    • fls=λt. λf. f\text{fls} = \lambda t.\ \lambda f.\ f
  • A generic "if" (conditional) combinator: test=λl. λm. λn. l m n\text{test} = \lambda l.\ \lambda m.\ \lambda n.\ l\ m\ n

    • test tru v wv\text{test}\ \text{tru}\ v\ w \to v
    • test fls v ww\text{test}\ \text{fls}\ v\ w \to w
  • Intuition: the boolean itself acts as the conditional:

    • tru v w\text{tru}\ v\ w chooses the first argument.
    • fls v w\text{fls}\ v\ w chooses the second argument.

Boolean operators

  • and=λb. λc. b c fls\text{and} = \lambda b.\ \lambda c.\ b\ c\ \text{fls}

    • If b=trub = \text{tru}, result is cc.
    • If b=flsb = \text{fls}, result is fls\text{fls}.
    • So and b c\text{and}\ b\ c is tru\text{tru} iff both bb and cc are tru\text{tru}.

Exercise 5.2.1 Define logical or\text{or} and not\text{not} functions.

Solution
  • or=λb. λc. b tru c\text{or} = \lambda b.\ \lambda c.\ b\ \text{tru}\ c
  • not=λb. λt. λf. b f t\text{not} = \lambda b.\ \lambda t.\ \lambda f.\ b\ f\ t

Pairs

  • Encode a pair of values using booleans:

    pair=λf. λs. λb. b f s\text{pair} = \lambda f.\ \lambda s.\ \lambda b.\ b\ f\ s

    fst=λp. p tru\text{fst} = \lambda p.\ p\ \text{tru}

    snd=λp. p fls\text{snd} = \lambda p.\ p\ \text{fls}

  • Intuition:

    • pair v w\text{pair}\ v\ w is a function expecting a boolean bb and returning b v wb\ v\ w.

    • If b=trub = \text{tru}, we get vv if b=flsb = \text{fls}, we get ww.

    • So:

      • fst (pair v w)v\text{fst}\ (\text{pair}\ v\ w) \to v
      • snd (pair v w)w\text{snd}\ (\text{pair}\ v\ w) \to w

Church numerals

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

    c0=λs. λz. zc_0 = \lambda s.\ \lambda z.\ z

    c1=λs. λz. s zc_1 = \lambda s.\ \lambda z.\ s\ z

    c2=λs. λz. s (s z)c_2 = \lambda s.\ \lambda z.\ s\ (s\ z)

    c3=λs. λz. s (s (s z))c_3 = \lambda s.\ \lambda z.\ s\ (s\ (s\ z))

    \dots

  • In general, cnc_n represents "apply ss to zz, nn times".

  • Note: c0c_0 and fls\text{fls} are actually the same term, just used with different intended meanings. This is interesting, as 00 is falsy in most programming languages.

  • Looks like fixed point approximation?

  • Successor (nn+1n \mapsto n + 1): scc=λn. λs. λz. s (n s z)\text{scc} = \lambda n.\ \lambda s.\ \lambda z.\ s\ (n\ s\ z)

  • Addition (m+nm + n):

    plus=λm. λn. λs. λz. m s (n s z)\text{plus} = \lambda m.\ \lambda n.\ \lambda s.\ \lambda z.\ m\ s\ (n\ s\ z)

    • First use n s zn\ s\ z to apply ss n times.
    • Then apply ss m more times via mm.
  • Multiplication (m×nm \times n) using plus:

    times=λm. λn. m (plus n) c0\text{times} = \lambda m.\ \lambda n.\ m\ (\text{plus}\ n)\ c_0

    • plus n\text{plus}\ n is a function "add nn".
    • m (plus n) c0m\ (\text{plus}\ n)\ c_0 means "start at 00, add nn, mm times" \to m×nm \times 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)\text{scc} = \lambda n.\ \lambda s.\ n\ s\ (c_1\ s\ z)

Exercise 5.2.3 Is it possible to define multiplication on Church numerals without using plus\text{plus}?

Solution

times=λm. λn. λs. λz. m (n s) z\text{times} = \lambda m.\ \lambda n.\ \lambda s.\ \lambda 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\text{pow} = \lambda m.\ \lambda n.\ \lambda s.\ \lambda z.\ m\ (\text{times}\ n)\ c_0

  • Zero test: iszro=λm. m (λx. fls) tru\text{iszro} = \lambda m.\ m\ (\lambda x.\ \text{fls})\ \text{tru}

  • iszro c0tru\text{iszro}\ c_0 \to \text{tru} (no steps applied). - iszro ckfls\text{iszro}\ c_k \to \text{fls} for k1k \ge 1 (at least one overwrite).

  • Predecessor:

  • Idea: track pairs (ci,ci+1)(c_i, c_{i+1}). - Helper definitions: - zz=pair c0 c0\text{zz} = \text{pair}\ c_0\ c_0 - ss=λp. pair (snd p) (succ (snd p))\text{ss} = \lambda p.\ \text{pair}\ (\text{snd}\ p)\ (\text{succ}\ (\text{snd}\ p))

  • Definition: prd=λm. fst (m ss zz)\text{prd} = \lambda m.\ \text{fst}\ (m\ \text{ss}\ \text{zz}) - Starting from (c0,c0)(c_0, c_0), each application of ss\text{ss} transforms

(ci,cj)(cj, cj+1)(c_i, c_j) \mapsto (c_j,\ c_{j+1})

  • After mm steps, we get (cm1,cm)(c_{m-1}, c_m) or (c0,c0)(c_0, c_0) when m=0m = 0. - Taking fst\text{fst} gives the predecessor.

Exercise 5.2.5 Use prd\text{prd} to define a subtraction function.

Solution

sub=λm. λn. n prd m\text{sub} = \lambda m.\ \lambda n.\ n\ \text{prd}\ m

Exercise 5.2.6 Approximately how many steps of evaluation (as a function of nn) are required to calculate prd cn\text{prd}\ c_n?

Solution

TODO

Exercise 5.2.7 Write a function equal\text{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))\text{equal} = \lambda m.\ \lambda n.\ \text{and}\ (\text{iszro}\ (\text{sub}\ m\ n))\ (\text{iszro}\ (\text{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\text{fold} function. (OCaml's name for this function is fold_left\text{fold\_left}; it is also sometimes called reduce\text{reduce}.) For example, the list [x,y,z][x,y,z] becomes a function that takes two arguments cc and nn and returns$c\ x\ (c\ y\ (c\ z\ n)))$. What would the representation of nil\text{nil} be? Write a function cons\text{cons} that takes an element hh and a list (that is, a fold\text{fold} function) tt and returns a similar representation of the list formed by prepending hh to tt. Write isnil\text{isnil} and head\text{head} functions, each taking a list parameter. Finally, write a tail\text{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\text{prd} for numbers).

Solution
  • nil=λc. λn. n\text{nil} = \lambda c.\ \lambda n.\ n
  • cons=λh. λt. λc. λn. (c h (t c n))\text{cons} = \lambda h.\ \lambda t.\ \lambda c.\ \lambda n.\ (c\ h\ (t\ c\ n))
  • isnil=λt. t (λe. λn. fls) tru\text{isnil} = \lambda t.\ t\ (\lambda e.\ \lambda n.\ \text{fls}) \ \text{tru}
  • head=λt. t tru nil\text{head} = \lambda t.\ t\ \text{tru}\ \text{nil}
  • tail=λt. fst (t tt nn)\text{tail} = \lambda t.\ \text{fst}\ (t\ tt\ nn)
  • nn=pair nil nilnn = \text{pair}\ \text{nil}\ \text{nil}
  • tt=λe. λp. pair (snd p) (cons e (snd p))tt = \lambda e.\ \lambda p.\ \text{pair}\ (\text{snd}\ p)\ (\text{cons}\ e\ (\text{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: 00 for numbers and nil\text{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\lambda c.\ \lambda z.\ z.
      • The nn-th value: λc. λz. c (vn1 c z)\lambda c.\ \lambda z.\ c\ (v_{n - 1 }\ c\ z).
    • For lists:

      • The 0-th value: λc. λn. n\lambda c.\ \lambda n.\ n.
      • The 1-st value: λc. λn. c e1 (v0 c n)\lambda c.\ \lambda n.\ c\ e_1\ (v_0\ c\ n) .
      • The nn-th value: λc. λn. c en (vn1 c n)\lambda c.\ \lambda n.\ c\ e_n\ (v_{n-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 λ\lambda.

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

  • Pure lambda-calculus enriched with booleans and naturals is denoted by λNB\lambda\textbf{NB}.

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

  • In λNB\lambda\textbf{NB}, 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\text{realbool} = \lambda b.\ b\ \text{true}\ \text{false}.

    • Primitive boolean \to Church boolean: churchbool=λb. if b then tru else fls\text{churchbool} = \lambda b.\ \text{if}\ b\ \text{then}\ \text{tru}\ \text{else}\ \text{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 c1c2\text{scc}\ c_1 \to c_2
    • But in CBV we actually get a \lambda-term that is extensionally equal to c2c_2, but not syntactically the same; some computation is "stuck under a \lambda" and cannot be reduced further under CBV.
    • So scc c1\text{scc}\ c_1 is behaviorally equivalent to c2c_2 (they act the same when given ss and zz), but not literally the same normal form.
  • Example: multiplication

    • times c2 c2\text{times}\ c_2\ c_2 does not reduce to c4c_4 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\text{equal}\ c_4\ (\text{times}\ c_2\ c_2) \;\to\; \text{tru}

      • Or more conveniently, converting to a primitive number:

        realnat (times c2 c2)    4\text{realnat}\ (\text{times}\ c_2\ c_2) \;\to\; 4

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

Therefore:

  • Encodings show that pure λ\lambda is expressive enough.
  • Primitives + conversions in λNB\lambda\textbf{NB} 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) \Omega = (\lambda x.\ x\ x)\ (\lambda x.\ x\ x)

    • This term has exactly one redex; reducing it just gives back the same term ω\omega again, so evaluation loops forever.
    • Any term with no normal form (like Ω\Omega) 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\text{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)) \text{fix} = \lambda f.\ (\lambda x.\ f\ (\lambda y.\ x\ x\ y))\ (\lambda x.\ f\ (\lambda y.\ x\ x\ y))

    • fix f\text{fix}\ f gives you a term hh such that, operationally,

      fix f f (fix f) \text{fix}\ f \approx\ f\ (\text{fix}\ f)

      so ff 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)) Y = \lambda f.\ (\lambda x.\ f\ (x\ x))\ (\lambda x.\ f\ (x\ x))

    but:

    • YY diverges under call-by-value.

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

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

  • Using fix\text{fix} to define recursive functions

    We want recursive definitions of the form:

    • Informal recursive style:

      h=body containing h h = \langle \text{body containing } h \rangle

    • To encode this in \lambda-calculus:

      1. First define a non-recursive generator:

        g=λf. body containing f g = \lambda f.\ \langle \text{body containing } f \rangle

      2. Then define the recursive function as:

        h=fix g h = \text{fix}\ g

    • Operational idea:

      • fix g\text{fix}\ g expands to something like g (fix g)g\ (\text{fix}\ g).

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

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

  • Example: factorial\text{factorial} with Church numerals

    g=λfct. λn. if realeq n c0  then c1  else times n (fct (prd n))factorial=fix g g = \lambda \text{fct}.\ \lambda n.\ \text{if } \text{realeq}\ n\ c_0\ \text{ then } c_1\ \text{ else } \text{times}\ n\ (\text{fct}\ (\text{prd}\ n)) \\ \text{factorial} = \text{fix}\ g

    • The fix\text{fix} machinery ensures that each time fct\text{fct} is applied.

      -> fct\text{fct} effectively behaves like factorial itself.

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

    • Conceptually, fct\text{fct} is a self-replicator:

      Applying fct n\text{fct}\ n feeds fct\text{fct} and nn back into gg.

      -> Producing another expanded copy of the recursive body, with new $\text{fct}$s ready to continue the process.

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

Solution
  • This is because if\text{if} doesn't need to evaluate both of its branches before evaluating itself, while test\text{test} needs to evaluate both of its branches first.
  • In other words, if\text{if} is lazy and test\text{test} is eager.
  • If we use test\text{test} in the same way as if\text{if} in the above example, it would yield divergent terms on every application.
  • How to define factorial\text{factorial} in terms of test\text{test}: Simulate call-by-name using thunks.

factorial n=λfct. λn. (test (equal n 0) (λ_. c1) (λ_. (times n (fct (sub n 1))))) tru\text{factorial}\ n = \lambda \text{fct}.\ \lambda n.\ (\text{test}\ (\text{equal}\ n\ 0)\ (\lambda \_.\ c_1)\ (\lambda \_.\ (\text{times}\ n\ (\text{fct}\ (\text{sub}\ n\ 1)))))\ \text{tru}

Exercise 5.2.10 Define a function churchnat\text{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 (n1))\text{churchnat} = \text{fix}\ \lambda\text{g}.\ \lambda n.\ \text{if } n = 0 \text{ then } c_0 \text{ else } \text{succ}\ (g\ (n - 1))

Exercise 5.2.11 Use fix\text{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))\text{sum} = \text{fix}\ \lambda g.\ \lambda l.\ \text{if } \text{isnil}\ l \text{ then }0\text{ else } \text{sum}\ (\text{head}\ l)\ (g\ (\text{tail}\ l))

Representation

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

  • Ordinary naturals (as in λNB\lambda\textbf{NB}):

    • A constant: 00.

    • Operations:

      • succ\text{succ}: numbers \to numbers.
      • pred\text{pred}: numbers \to numbers.
      • iszero\text{iszero}: numbers \to booleans.
    • Their behavior is fixed by evaluation rules (e.g. succ 2=3\text{succ}\ 2 = 3, iszero 0=true\text{iszero}\ 0 = \text{true}).

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

    • Zero: c0=λs. λz. zc_0 = \lambda s.\ \lambda z.\ z

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

      • scc\text{scc} represents succ\text{succ}: if tt represents nn, then scc t\text{scc}\ t evaluates to a representation of n+1n + 1.
      • prd\text{prd} represents pred\text{pred}: if tt represents nn, then prd t\text{prd}\ t evaluates to a representation of max(n1,0)\max(n-1, 0).
    • Zero test: iszro\text{iszro} represents iszero\text{iszero}:

      • If tt represents 0, iszro t\text{iszro}\ t evaluates to true\text{true}.
      • If tt represents any n0n \neq 0, iszro t\text{iszro}\ t evaluates to false\text{false}.
  • Representation correctness (observational view):

    • Take any program that:

      • Uses primitive numbers and operations (00, succ\text{succ}, pred\text{pred}, iszero\text{iszero}, \ldots)
      • Produces a boolean result.
    • Replace all numbers and arithmetic operations with their Church encodings (c0c_0, scc\text{scc}, prd\text{prd}, iszro\text{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 tt ::= x\ \vert \ \lambdax.t\ \vert \ t\ t) is shorthand for an inductively defined set of abstract syntax trees.

  • Terms:

    • Fix a countable set of variable names VV.

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

      1. xT for every xVx \in T \ \text{for every } x \in V.
      2. if t1T and xV, then λx. t1T\text{if } t_1 \in T \text{ and } x \in V,\text{ then } \lambda x.\ t_1 \in T.
      3. if t1T and t2T, then t1 t2T\text{if } t_1 \in T \text{ and } t_2 \in T,\text{ then } t_1\ t_2 \in T.
  • Free variables (FV\text{FV})

    • FV(t)\text{FV}(t) = set of variables that occur free in term tt.

    • Rules:

      1. FV(x)=x\text{FV}(x) = {x}.
      2. FV(λx. t1)=FV(t1)x\text{FV}(\lambda x.\ t_1) = \text{FV}(t_1) \setminus {x}.
      3. FV(t1 t2)=FV(t1)FV(t2)\text{FV}(t_1\ t_2) = \text{FV}(t_1) \cup \text{FV}(t_2).

Exercise 5.3.3 Give a careful proof that FV(t)size(t)\vert \text{FV}(t) \vert \leq \text{size}(t) for every term tt.

Solution

TODO

Substitution

  • Throughout, two definitions of substitution are used:

    • The compact and intuitive definition shown before: [xs][x \to 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 [xs]t[x \mapsto s] t in the \lambda-calculus correctly (capture-avoiding).

Naive substitution (wrong #1)

  • Defined structurally on term tt:

    • [xs]x=s[x \mapsto s] x = s.
    • [xs]y=yif xy[x \mapsto s] y = y \quad \text{if } x \ne y
    • [xs](λy. t1)=λy. [xs] t1[x \mapsto s] (\lambda y.\ t_1) = \lambda y.\ [x \mapsto s]\ t_1
    • [xs](t1 t2)=([xs] t1) ([xs] t2)[x \mapsto s] (t_1\ t_2) = ([x \mapsto s]\ t_1)\ ([x \mapsto s]\ t_2)
  • Problem: does not distinguish free vs bound occurrences of xx.

    Example: [xy](λx. x)=λx. y[x \mapsto y](\lambda x.\ x) = \lambda 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\lambdax.\ x or \lambday. y\lambday.\ y or λfranz. franz\lambda\text{franz}.\ \text{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:

    • [xs]x=s[x \mapsto s] x = s
    • [xs]y=yif yx[x \mapsto s] y = y \quad \text{if } y \ne x
    • [xs](λy. t1)={λy. t1if y=xλy. [xs] t1if yx[x \mapsto s] (\lambda y.\ t_1) = \begin{cases} \lambda y.\ t_1 & \text{if } y = x \\ \lambda y.\ [x \mapsto s]\ t_1 & \text{if } y \ne x \end{cases}

    • [xs](t1 t2)=([xs] t1) ([xs] t2)[x \mapsto s] (t_1\ t_2) = ([x \mapsto s]\ t_1)\ ([x \mapsto s]\ t_2)
  • Fixes earlier issue, but introduces variable capture:

    Example: [xz](λz. x)=λz. z[x \mapsto z](\lambda z.\ x) = \lambda z.\ z

Variable capture & capture-avoiding substitution

  • Variable capture: a free variable in ss becomes bound after substitution into tt.

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

    • is not xx, and
    • does not occur free in ss.
  • Capture-avoiding substitution:

    • [xs]x=s[x \mapsto s] x = s
    • [xs]y=yif yx[x \mapsto s] y = y \quad \text{if } y \ne x
    • [xs](λy. t1)={λy. t1if y=xλy. [xs] t1if yx and yFV(s)[x \mapsto s] (\lambda y.\ t_1) = \begin{cases} \lambda y.\ t_1 & \text{if } y = x \\ \lambda y.\ [x \mapsto s]\ t_1 & \text{if } y \ne x \text{ and } y \notin FV(s) \end{cases}

    • [xs](t1 t2)=([xs] t1) ([xs] t2)[x \mapsto s] (t_1\ t_2) = ([x \mapsto s]\ t_1)\ ([x \mapsto s]\ t_2)
  • The above definition is partial: if yxy \neq x but yFV(s)y \in \text{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\lambday.\ x\ y \equiv \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 yFV(s)y \in \text{FV}(s), we rename the bound variable first.

    Example:

    [xy z](λy. x y)[x \mapsto y\ z](\lambda y.\ x\ y)

    • First rename: \lambday. x y\lambday.\ x\ y \to \lambdaw. x w\lambdaw.\ x\ w
    • Then: [xy z](λw. x w)=λw. (y z w)[x \mapsto y\ z](\lambda w.\ x\ w) = \lambda w.\ (y\ z\ w)

Final capture-avoiding substitution definition

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

    • [xs]x=s[x \mapsto s] x = s
    • [xs]y=yif yx[x \mapsto s] y = y \quad \text{if } y \ne x
    • [xs](λy. t1)=λy. [xs] t1if yx and yFV(s)[x \mapsto s] (\lambda y.\ t_1) = \lambda y.\ [x \mapsto s]\ t_1 \quad \text{if } y \ne x \text{ and } y \notin \text{FV}(s)
    • [xs](t1 t2)=([xs] t1) ([xs] t2)[x \mapsto s] (t_1\ t_2) = ([x \mapsto s]\ t_1)\ ([x \mapsto s]\ t_2)
  • 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. tv ::= \lambda x.\ t
    • Evaluation stops when it reaches a λ\lambda; arbitrary λ\lambda-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

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

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

    • vv 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 t1t_1 to a value (E-App1).
      2. Reduce t2t_2 to a value (E-App2).
      3. Apply \beta (E-AppAbs).
  • Special property of the pure \lambda-calculus

    • Since only \lambda-abstractions are values, once t1t_1 has been reduced to a value (by E-App1), it must be a \lambdax. t\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. t1t1t1 t2t1 t2\displaystyle \frac{t_1 \to t_1' }{t_1\ t_2 \to t_1'\ t_2}
    2. t2t2t1 t2t1 t2\displaystyle \frac{t_2 \to t_2'}{t_1\ t_2 \to t_1\ t_2'}
    3. ttλx. tλx. t\displaystyle \frac{t \to t'}{\lambda x.\ t \to \lambda x.\ t'}
    4. (λx. t1) t2[xt2]t1(\lambda x.\ t_1)\ t_2 \to [x \to t_2] t_1
  • Normal-order

    1. (λx. t1) t2[xt2]t1(\lambda x.\ t_1)\ t_2 \to [x \mapsto t_2]\, t_1
    2. t1t1t1 t2t1 t2(t1 is not a λ)\displaystyle \frac{t_1 \to t_1'}{t_1\ t_2 \to t_1'\ t_2} \quad (t_1 \text{ is not a } \lambda)
    3. t2t2t1 t2t1 t2(t1 in normal form)\displaystyle \frac{t_2 \to t_2'}{t_1\ t_2 \to t_1\ t_2'} \quad (t_1 \text{ in normal form})
    4. ttλx. tλx. t\displaystyle \frac{t \to t'}{\lambda x.\ t \to \lambda x.\ t'}
  • Call-by-name

    1. \displaystyle t1t1t1 t2t1 t2\displaystyle \frac{t_1 \to t_1' }{t_1\ t_2 \to t_1'\ t_2}
    2. (λx. t1) t2[xt2]t1(\lambda x.\ t_1)\ t_2 \to [x \to t_2] t_1

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 λNB\lambda\textbf{NB}.

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 tt evaluates to final result vv." Show how to formulate the evaluation rules for lambda-terms in the big-step style.

Solution
  1. t1λx. t1t2v2[xv2]t1vt1 t2v\displaystyle \frac{t_1 \Downarrow \lambda x.\ t_1' \quad t_2 \Downarrow v_2 \quad [x \to v_2]t_1' \Downarrow v}{t_1\ t_2 \Downarrow v}
  2. [xt2]t1v(λx. t1) t2v\displaystyle \frac{[x\to t_2]t_1 \Downarrow v}{ (\lambda x.\ t_1)\ t_2 \Downarrow v}