Working System
This chapter works in the untyped lambda calculus, represented as a Python AST (Var, Lam, App).
1. Lambda Calculus Basics: Syntax and Variables#
Goal. Build a precise reading of lambda terms, then implement a small Python representation to experiment with free and bound variables.
You will learn
The grammar of lambda terms
How to read and parenthesize terms
Free and bound variables
Alpha-renaming as a safe change of bound variable names
Roadmap#
Define the syntax of lambda terms and the standard parenthesization conventions.
Implement an AST (
Var,Lam,App) and a readable printershow.Define and compute free variables.
Implement alpha-renaming and test it on shadowing/capture examples.
Definition (Lambda Terms)
A term is built from three constructors.
A variable:
xAn abstraction:
λx. tAn application:
t u
Definition (Precedence and Associativity)
We use two conventions.
Application associates to the left:
t u vmeans(t u) v.Abstraction extends to the right:
λx. t umeansλx. (t u).
Example
λx. x (λy. y) means λx. (x (λy. y)), while (λx. x) (λy. y) applies the identity function to itself.
We’ll implement a small abstract syntax tree (AST) and some helper functions to reason about variables.
from dataclasses import dataclass
from typing import Set, Union
@dataclass(frozen=True)
class Var:
name: str
@dataclass(frozen=True)
class Lam:
param: str
body: "Term"
@dataclass(frozen=True)
class App:
fn: "Term"
arg: "Term"
Term = Union[Var, Lam, App]
def show(t: Term) -> str:
if isinstance(t, Var):
return t.name
if isinstance(t, Lam):
return f"(λ{t.param}. {show(t.body)})"
if isinstance(t, App):
return f"({show(t.fn)} {show(t.arg)})"
raise TypeError("Unknown term")
def free_vars(t: Term) -> Set[str]:
if isinstance(t, Var):
return {t.name}
if isinstance(t, Lam):
return free_vars(t.body) - {t.param}
if isinstance(t, App):
return free_vars(t.fn) | free_vars(t.arg)
raise TypeError("Unknown term")
Example
Let t = (λx. x) (λy. y).
The function and argument are both abstractions.
No variables are free.
t = App(Lam("x", Var("x")), Lam("y", Var("y")))
print(show(t))
print(free_vars(t))
((λx. x) (λy. y))
set()
Definition (Free vs. Bound)
A variable occurrence is bound if it lies inside the scope of a matching λx. It is free otherwise.
Free variables act like parameters from the outside world.
Definition (Alpha-Renaming)
Alpha-renaming changes the name of a bound variable, keeping the term’s meaning the same.
Example: λx. x is alpha-equivalent to λz. z.
We will implement a safe bound-variable rename inside a lambda body.
Rules:
Do not rename inside an inner lambda that binds the same variable (shadowing).
The target binder must be fresh in the body to avoid capture.
If the requested name is not fresh, we automatically generate a fresh variant.
def all_vars(t: Term) -> Set[str]:
if isinstance(t, Var):
return {t.name}
if isinstance(t, Lam):
return {t.param} | all_vars(t.body)
if isinstance(t, App):
return all_vars(t.fn) | all_vars(t.arg)
raise TypeError("Unknown term")
def fresh_var(used: Set[str], hint: str = "x") -> str:
i = 0
while True:
candidate = f"{hint}{i}"
if candidate not in used:
return candidate
i += 1
def rename_bound(body: Term, old: str, new: str) -> Term:
if isinstance(body, Var):
return Var(new) if body.name == old else body
if isinstance(body, App):
return App(rename_bound(body.fn, old, new), rename_bound(body.arg, old, new))
if isinstance(body, Lam):
if body.param == old:
# Shadowing: stop here
return body
return Lam(body.param, rename_bound(body.body, old, new))
raise TypeError("Unknown term")
def alpha_rename(lam_term: Lam, new_param: str) -> Lam:
if new_param == lam_term.param:
return lam_term
used = all_vars(lam_term.body) | {lam_term.param}
target = new_param if new_param not in used else fresh_var(used | {new_param}, new_param)
return Lam(target, rename_bound(lam_term.body, lam_term.param, target))
Example
Alpha-renaming changes bound names but keeps meaning.
term = Lam("x", App(Var("x"), Var("y")))
renamed = alpha_rename(term, "z")
print(show(term))
print(show(renamed))
# Requesting binder "y" would capture free y; alpha_rename makes it fresh automatically.
capture_sensitive = Lam("x", App(Var("x"), Var("y")))
print(show(alpha_rename(capture_sensitive, "y")))
(λx. (x y))
(λz. (z y))
(λy0. (y0 y))
Exercise
Compute the free variables of
λx. (x (y z)).Alpha-rename the outer binder in
λx. (λx. x) xtow. Which occurrences ofxshould change?Give a term where renaming a binder to a free variable name would change meaning (capture). What does a capture-avoiding alpha-rename do instead?
For each term, compute the set of free variables: (a)
λx. λy. x y, (b)λx. y x, ©(λx. x) y. See solution: Exercise 1 (Lambda Basics)
Summary#
Lambda terms are built from variables, abstractions, and applications.
Parenthesization conventions matter: application is left-associative; abstraction extends to the right.
Free variables are computed structurally; bound variables are those introduced by a surrounding binder.
Alpha-renaming changes only a chosen binder and the occurrences it binds (shadowing stops renaming).
Capture-avoiding alpha-renaming never turns a free variable into a bound one.
Operationally: edit a term in the code cells and re-run
free_vars(...)andshow(...)to see how binding changes.Next: capture-avoiding substitution and beta-reduction (Chapter 2).