Skip to main content

Untyped and Typed Lambda Calculus

A graphical lambda-calculus reduction diagram shows beta-reduction transforming an application into a substituted term.

Figure: Lambda terms reduce by beta-reduction, substituting argument for parameter. Image: Wikimedia Commons, CC BY-SA.

The lambda calculus is the small core language behind much of programming language theory: a program is a term, computation is substitution, and functions are ordinary values. TAPL treats it operationally, with careful attention to binding, reduction strategies, and implementations; the semantics text treats it as one of several formal semantic models; Software Foundations uses similar syntax and proof ideas to mechanize reasoning about small languages [1], [2], [3]. This page combines those angles: syntax first, then reduction, then typing.

The untyped calculus is powerful enough to encode natural numbers, booleans, recursion, and all computable functions, but it also admits meaningless self-application and nontermination. The simply typed lambda calculus (STLC) restricts terms with types, which rules out many stuck programs and gives a clean first example of type soundness. The price is expressiveness: plain STLC is strongly normalizing and cannot define general recursion without adding a fixed-point operator.

Definitions

The pure untyped lambda calculus has terms

t::=xλx.tt tt ::= x \mid \lambda x.t \mid t\ t

where xx ranges over variables. A variable occurrence is bound if it lies inside the body of a matching λx\lambda x; otherwise it is free. The set of free variables is defined by

FV(x)={x}FV(λx.t)=FV(t){x}FV(t1 t2)=FV(t1)FV(t2).\begin{aligned} FV(x) &= \{x\} \\ FV(\lambda x.t) &= FV(t) \setminus \{x\} \\ FV(t_1\ t_2) &= FV(t_1) \cup FV(t_2). \end{aligned}

A term is closed when FV(t)=FV(t)=\emptyset. Substitution [xs]t[x \mapsto s]t replaces free occurrences of xx in tt by ss, avoiding variable capture by renaming bound variables when necessary. The central computational rule is beta-reduction:

(λx.t) sβ[xs]t.(\lambda x.t)\ s \to_\beta [x \mapsto s]t.

Alpha-conversion renames bound variables without changing meaning: λx.x\lambda x.x and λy.y\lambda y.y are the same up to alpha-equivalence. Eta-reduction expresses extensional equality of functions:

λx.(f x)ηfwhen xFV(f).\lambda x.(f\ x) \to_\eta f \quad \text{when } x \notin FV(f).

A redex is a reducible expression, usually a beta-redex. A term is in normal form when it contains no redex. A reduction strategy chooses which redex to contract. Call-by-name reduces the outermost redex without first evaluating the argument. Call-by-value reduces an application only after the argument is a value, normally a lambda abstraction.

STLC adds types:

T::=BoolNatTTT ::= \textsf{Bool} \mid \textsf{Nat} \mid T \to T

and typing judgments Γt:T\Gamma \vdash t : T, where Γ\Gamma maps variables to types. The core rules are:

x:TΓΓx:TΓ,x:T1t:T2Γλx:T1.t:T1T2Γt1:T1T2Γt2:T1Γt1 t2:T2.\frac{x:T \in \Gamma}{\Gamma \vdash x:T} \qquad \frac{\Gamma,x:T_1 \vdash t:T_2}{\Gamma \vdash \lambda x:T_1.t : T_1 \to T_2} \qquad \frac{\Gamma \vdash t_1:T_1 \to T_2 \quad \Gamma \vdash t_2:T_1}{\Gamma \vdash t_1\ t_2:T_2}.

The Curry-Howard correspondence reads T1T2T_1 \to T_2 as implication, a lambda abstraction as a proof that assumes T1T_1 and derives T2T_2, and application as modus ponens [2], [4].

Key results

Church-Rosser confluence. If tut \to^* u and tvt \to^* v, then there exists ww such that uwu \to^* w and vwv \to^* w. Confluence means the final normal form, if it exists, is independent of the reduction path [5]. The proof is usually not by naive local confluence, because ordinary beta-reduction is not strongly normalizing in the untyped calculus. Standard presentations introduce parallel reduction, prove the diamond property for it, and show that ordinary beta-reduction has the same reflexive transitive closure.

Normal-order completeness for normalization. If an untyped term has a normal form, leftmost-outermost reduction finds it. This explains why call-by-name can terminate on examples where call-by-value diverges. For example, (λx.y) Ω(\lambda x.y)\ \Omega, where Ω=(λz.z z)(λz.z z)\Omega=(\lambda z.z\ z)(\lambda z.z\ z), reduces to yy under call-by-name but loops under call-by-value because the argument is evaluated first.

Expressiveness of encodings. Church numerals encode nn as a higher-order iterator:

n=λf.λx.fnx.\overline{n} = \lambda f.\lambda x. f^n x.

For instance, 2=λf.λx.f(fx)\overline{2}=\lambda f.\lambda x.f(fx) and successor is

succ=λn.λf.λx.f(n f x).\mathsf{succ}=\lambda n.\lambda f.\lambda x.f(n\ f\ x).

This supports arithmetic without primitive numbers, showing that syntax plus beta-reduction already carries computation.

Fixed points and recursion. In the untyped calculus, the combinator

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

satisfies YFβF(YF)YF \to_\beta F(YF). Thus recursion can be expressed as a fixed point. In strict call-by-value languages, the naive YY forces self-application too early; a value-friendly variant such as ZZ delays the recursive call under a lambda.

STLC progress and preservation. For a closed well-typed STLC term, either the term is a value or it can step. If a well-typed term steps, its type is preserved. Together, these theorems say well-typed STLC programs do not get stuck [1], [2].

Strong normalization of pure STLC. Every well-typed term in pure STLC terminates. TAPL presents normalization as an optional deeper result; its proof is typically by logical relations, assigning to each type a set of strongly normalizing terms. This theorem sharply separates STLC from the untyped calculus: self-application λx.x x\lambda x.x\ x is not typable in plain STLC.

Binding representations matter. The paper notation uses named variables because it is readable, but implementations and mechanized proofs often switch representations. A named representation needs alpha-renaming and freshness side conditions. De Bruijn indices replace variable names by numbers counting binders, so λx.λy.x\lambda x.\lambda y.x becomes λ.λ.1\lambda.\lambda.1 and λa.λb.a\lambda a.\lambda b.a has the same representation. This removes alpha-equivalence as a separate quotient but makes shifting and substitution delicate. Locally nameless representations combine named free variables with nameless bound variables. TAPL uses nameless terms in an implementation chapter precisely because the mathematical convenience of "rename as needed" must eventually become an algorithm [1].

Source emphasis. TAPL treats the calculus as a running substrate for type systems, usually with call-by-value rules because they align with ML-family implementation. The formal-semantics source presents lambda calculus beside abstract machines such as SECD, emphasizing how reduction can be implemented as state transitions [3]. Software Foundations-style developments care about the same definitions but expose proof obligations that paper presentations hide: every substitution lemma, free-variable fact, and context convention must be explicit. A useful study habit is to read each beta-reduction both as informal algebra and as a candidate rule in an interpreter; any ambiguity about variables will show up immediately in the interpreter.

Visual

ConceptUntyped calculusSTLC
Function syntaxλx.t\lambda x.tλx:T.t\lambda x:T.t
Applicationunrestrictedargument type must match domain
Self-applicationallowedusually rejected
Recursiondefinable with YYneeds an added fix operator or recursive types
Normalizationnot guaranteedguaranteed for pure STLC
Main proof burdenconfluence, substitutionsubstitution, progress, preservation

Worked example 1: reducing a Church numeral

Problem: show that succ 2\mathsf{succ}\ \overline{2} behaves like 3\overline{3}.

Use the definitions:

2=λf.λx.f(fx)succ=λn.λf.λx.f(n f x).\begin{aligned} \overline{2} &= \lambda f.\lambda x.f(fx) \\ \mathsf{succ} &= \lambda n.\lambda f.\lambda x.f(n\ f\ x). \end{aligned}

Step 1: apply successor to 2\overline{2}.

succ 2=(λn.λf.λx.f(n f x))(λf.λx.f(fx))βλf.λx.f((λf.λx.f(fx)) f x).\begin{aligned} \mathsf{succ}\ \overline{2} &= (\lambda n.\lambda f.\lambda x.f(n\ f\ x))(\lambda f.\lambda x.f(fx)) \\ &\to_\beta \lambda f.\lambda x.f((\lambda f.\lambda x.f(fx))\ f\ x). \end{aligned}

Step 2: reduce the encoded two inside the body. Rename bound variables to avoid confusion:

(λg.λy.g(gy)) f xβ(λy.f(fy)) xβf(fx).\begin{aligned} (\lambda g.\lambda y.g(g y))\ f\ x &\to_\beta (\lambda y.f(fy))\ x \\ &\to_\beta f(fx). \end{aligned}

Step 3: substitute the result back into the outer body:

λf.λx.f(f(fx)).\lambda f.\lambda x.f(f(fx)).

This is exactly 3\overline{3}. A quick check is to apply it to a concrete function SS and seed ZZ:

(λf.λx.f(f(fx))) S ZS(S(SZ)).(\lambda f.\lambda x.f(f(fx)))\ S\ Z \to S(S(SZ)).

Worked example 2: why self-application is not typable in STLC

Problem: try to assign a type to λx.x x\lambda x.x\ x in STLC.

Step 1: assume the abstraction has some type ABA \to B. Then under the context x:Ax:A, the body x xx\ x must have type BB:

x:Ax x:B.x:A \vdash x\ x : B.

Step 2: by the application typing rule, the left occurrence of xx must have a function type CBC \to B and the right occurrence must have type CC:

x:Ax:CBx:Ax:C.x:A \vdash x:C \to B \qquad x:A \vdash x:C.

Step 3: by the variable rule, both occurrences of xx have the type assigned in the context, so

A=CBA=C.A = C \to B \qquad A = C.

Step 4: combine the equations:

C=CB.C = C \to B.

Plain STLC has finite simple types built from base types and arrows. No finite type CC can equal CBC \to B, because the right side has one more outer arrow constructor. Therefore the original term is untypable. This is not a syntactic accident; it is the mechanism that blocks the usual untyped fixed-point construction.

Code

from dataclasses import dataclass

@dataclass(frozen=True)
class Var:
name: str

@dataclass(frozen=True)
class Lam:
param: str
body: object

@dataclass(frozen=True)
class App:
fn: object
arg: object

def free(t):
if isinstance(t, Var):
return {t.name}
if isinstance(t, Lam):
return free(t.body) - {t.param}
return free(t.fn) | free(t.arg)

def subst(t, x, s):
if isinstance(t, Var):
return s if t.name == x else t
if isinstance(t, Lam):
if t.param == x:
return t
if t.param in free(s):
raise ValueError("alpha-rename before substituting")
return Lam(t.param, subst(t.body, x, s))
return App(subst(t.fn, x, s), subst(t.arg, x, s))

def step_cbv(t):
if isinstance(t, App) and isinstance(t.fn, Lam) and isinstance(t.arg, Lam):
return subst(t.fn.body, t.fn.param, t.arg)
if isinstance(t, App) and not isinstance(t.fn, Lam):
return App(step_cbv(t.fn), t.arg)
if isinstance(t, App) and not isinstance(t.arg, Lam):
return App(t.fn, step_cbv(t.arg))
raise StopIteration("normal form or stuck")

Common pitfalls

  • Treating alpha-equivalent terms as different programs; bound variable names are not semantically important.
  • Performing substitution without capture avoidance, which can silently change a term's meaning.
  • Assuming every term has a normal form; Ω\Omega is the standard counterexample.
  • Confusing call-by-name with call-by-need; call-by-need shares evaluated thunks.
  • Using the untyped YY combinator directly in a strict language without delaying recursion.
  • Forgetting that eta-reduction requires xFV(f)x \notin FV(f).
  • Believing Church numerals are efficient integers; they are conceptual encodings, not practical machine representations.
  • Thinking confluence implies termination; it only says different terminating paths agree.
  • Typing application by matching codomain first; the argument must match the function domain.
  • Ignoring contexts in typing derivations; variables receive types only through Γ\Gamma.
  • Trying to type λx.x x\lambda x.x\ x by inventing an infinite simple type; STLC does not have recursive types.
  • Assuming STLC has recursion by default; adding fix changes normalization.

Connections

References

[1] B. C. Pierce, Types and Programming Languages. MIT Press, 2002.
[2] B. C. Pierce et al., Software Foundations, electronic textbook series.
[3] K. Slonneger and B. L. Kurtz, Formal Syntax and Semantics of Programming Languages. Addison-Wesley, 1995.
[4] W. A. Howard, "The formulae-as-types notion of construction," 1980.
[5] A. Church and J. B. Rosser, "Some properties of conversion," Transactions of the American Mathematical Society, 1936.
[6] H. P. Barendregt, The Lambda Calculus: Its Syntax and Semantics. North-Holland, 1984.