Lambda Calculus

· 6 min · 1,360 words

topic/sciencecomputer science/programming languages

Motivation: From Python to Pure Math

In modern programming, we are used to defining named functions that operate on built-in data types. For example, in Python:

def add(x, y):
return x + y

But Python also supports anonymous functions, using the lambda keyword:

add = lambda x, y: x + y

Notice what happened here: the fundamental concept of “addition” doesn’t strictly need a name attached to it. It is just a mapping of inputs to an output.

Invented by Alonzo Church in the 1930s, the λ\lambda-calculus asks a radical question: What if we take this to the absolute extreme? What if a programming language had no built-in data types, no state, no memory, and no named functions? There are no integers, no booleans, and no variables to mutate. Everything—literally every piece of data and control flow—is represented purely by anonymous functions and how they are applied to one another.

Computation here is not “mutating a map.” It is a mechanical process of textual substitution.

1. Syntax: The Ultimate Minimalism

The syntax is remarkably tiny. Every valid expression (ee) falls into one of three categories:

e::=xλx.ee1e2e ::= x \mid \lambda x. e \mid e_1 e_2
  1. Variables (xx): A placeholder (e.g., x,y,zx, y, z).
  2. Abstraction (λx.e\lambda x. e): A function definition. It binds the variable xx in the expression ee. You can read this as “a function that takes an argument xx and returns ee.”
  3. Application (e1e2e_1 e_2): A function call. It means “apply the function e1e_1 to the argument e2e_2.”

(Note: Function application is left-associative, so e1e2e3e_1 e_2 e_3 means (e1e2)e3(e_1 e_2) e_3. Function bodies extend as far right as possible, so λx.xy\lambda x. x y means λx.(xy)\lambda x. (x y)).

2. The Three Mechanics of Computation

A program runs by rewriting itself until it can’t be rewritten anymore (reaching “normal form”). This relies on three rules:

α\alpha-Conversion (Renaming)

The name of a bound variable doesn’t matter. A function that takes xx and returns xx is identical to a function that takes yy and returns yy.

λx.xλy.y\lambda x. x \equiv \lambda y. y


Why it matters: We need to rename variables to avoid “variable capture” (accidentally overwriting a variable from an outer scope when doing substitutions).

β\beta-Reduction (Execution)

This is the actual engine of computation. When a function is applied to an argument, you substitute the argument into the function’s body wherever the bound variable appears.

(λx.e1)e2e1[xe2](\lambda x. e_1) e_2 \rightarrow e_1[x \mapsto e_2]
  • e1[xe2]e_1[x \mapsto e_2] reads as: “in expression e1e_1, replace all free occurrences of xx with e2e_2.”
  • Example: (λx.xy)zzy(\lambda x. x y) z \rightarrow z y

η\eta-Conversion (Extensionality)

Two functions are the same if they yield the same result for all arguments. If a function just wraps another function, the wrapper is redundant.

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

3. Operational Semantics (Evaluation Strategies)

When you have a complex expression like (λx.e)((λy.y)z)(\lambda x. e) ((\lambda y. y) z), what do you reduce first? The function body, or the argument? Let vv represent a value (a fully evaluated abstraction λx.e\lambda x. e).

Call-by-Value (Strict / Eager)

Used by most languages (Python, Java, OCaml). You must fully evaluate the argument into a value vv before you substitute it into the function.

  1. Evaluate left side: e1e1e1e2e1e2\frac{e_1 \rightarrow e_1'}{e_1 e_2 \rightarrow e_1' e_2}
  2. Evaluate right side: e2e2v1e2v1e2\frac{e_2 \rightarrow e_2'}{v_1 e_2 \rightarrow v_1 e_2'}
  3. Apply: (λx.e)ve[xv]\frac{}{(\lambda x. e) v \rightarrow e[x \mapsto v]}

Call-by-Name (Lazy)

Used in languages like Haskell. You pass the argument unevaluated. It only gets evaluated if the function body actually uses it.

  1. Evaluate left side: e1e1e1e2e1e2\frac{e_1 \rightarrow e_1'}{e_1 e_2 \rightarrow e_1' e_2}
  2. Apply immediately: (λx.e1)e2e1[xe2]\frac{}{(\lambda x. e_1) e_2 \rightarrow e_1[x \mapsto e_2]}

Example: The Infinite Loop Test

To clearly see the difference in practice, imagine a function that ignores its argument, and an argument that loops forever.

  • Call-by-Value (e.g., Python):
def ignore(x):
    return 0

def loop():
    while True: pass

ignore(loop()) # The program freezes here!

Python eagerly evaluates loop() before passing its value to ignore. Since loop() never finishes, the program hangs indefinitely.

  • Call-by-Name / Lazy (e.g., Haskell):
ignore x = 0
loop = loop

main = print (ignore loop) -- Instantly prints 0

Haskell passes the unevaluated loop expression directly into ignore. Because the body of ignore never actually uses x, loop is never evaluated, and the program terminates instantly!

4. Church Encodings: Creating Data from Nothing

How is this Turing complete if there are no booleans or numbers? We “fake” them by defining behaviors using pure functions.

Booleans

A boolean represents a choice between two things. So, we define TRUE as a function that takes two arguments and returns the first. FALSE takes two arguments and returns the second.

  • TRUEλx.λy.x\text{TRUE} \equiv \lambda x. \lambda y. x
  • FALSEλx.λy.y\text{FALSE} \equiv \lambda x. \lambda y. y

An IF-statement is just applying the boolean to the two branches:

  • IFλb.λt.λf.btf\text{IF} \equiv \lambda b. \lambda t. \lambda f. b t f

Example: IF TRUE M N \rightarrow TRUE M N (λx.λy.x)MN(λy.M)NM\rightarrow (\lambda x. \lambda y. x) M N \rightarrow (\lambda y. M) N \rightarrow M. The control flow emerges entirely from substitution!

Church Numerals

How do we represent numbers? A number nn is represented by a function that takes another function ff and an argument xx, and applies ff to xx exactly nn times.

  • 0λf.λx.x0 \equiv \lambda f. \lambda x. x (apply ff zero times)
  • 1λf.λx.fx1 \equiv \lambda f. \lambda x. f x (apply ff once)
  • 2λf.λx.f(fx)2 \equiv \lambda f. \lambda x. f (f x) (apply ff twice)
  • 3λf.λx.f(f(fx))3 \equiv \lambda f. \lambda x. f (f (f x))

We can even define a Successor function (SUCC) that takes a Church numeral nn and returns n+1n+1:

  • SUCCλn.λf.λx.f(nfx)\text{SUCC} \equiv \lambda n. \lambda f. \lambda x. f (n f x) (Read as: take a number nn, and apply ff one more time to the result of applying ff nn times to xx.)

5. Recursion and the Y-Combinator

Here is the ultimate puzzle: How do you write a recursive function (like factorial) if functions don’t have names? You can’t call yourself if you don’t know who you are.

The trick is to pass the function to itself as an argument. To standardize this, logician Haskell1 Curry discovered the Y-Combinator2, a “fixed-point combinator”:

Yλf.(λx.f(xx))(λx.f(xx))Y \equiv \lambda f. (\lambda x. f (x x)) (\lambda x. f (x x))

What happens if we apply YY to some function gg? Let’s β\beta-reduce it:

  1. Yg=(λf.(λx.f(xx))(λx.f(xx)))gY g = (\lambda f. (\lambda x. f (x x)) (\lambda x. f (x x))) g
  2. Substitute gg for ff: (λx.g(xx))(λx.g(xx))\rightarrow (\lambda x. g (x x)) (\lambda x. g (x x))
  3. Substitute the right block for xx in the left block: g((λx.g(xx))(λx.g(xx)))\rightarrow g ((\lambda x. g (x x)) (\lambda x. g (x x)))

Notice that the giant block inside the parenthesis is exactly step 2 again! So:

Ygg(Yg)Y g \rightarrow g (Y g)

The Magic: YgY g evaluates to gg wrapped around another copy of YgY g. If gg is a step in a recursive function (like one iteration of factorial), YY automatically unpacks an infinite supply of gg‘s whenever needed, effectively creating a while loop or recursion out of thin air, using purely anonymous textual substitution.

Footnotes

  1. The Man With Three Languages: Haskell Curry is such a foundational figure in programming language theory that he has three different programming languages named after him: Haskell, Brooks (his middle name), and Curry. Aside from his work on combinatory logic, he is most famous for “Currying”—the technique of transforming a function that takes multiple arguments into a sequence of functions that each take a single argument (e.g., converting f(x, y) into f(x)(y)). If you’ve ever wondered why the λ\lambda-calculus only deals with single-argument functions, Currying is the mathematical justification that proves a single argument is all you ever really need!

  2. The Startup Connection: If “Y Combinator” sounds familiar outside of academic computer science, it’s because Paul Graham chose it as the name for his legendary Silicon Valley startup accelerator (the “wet dream” of many founders, having backed Airbnb, Stripe, Reddit, etc.). The metaphor is intentional: just as the mathematical Y-combinator calculates a “fixed point” that allows a program to bootstrap its own execution out of purely anonymous functions, the accelerator aims to act as the ultimate bootstrap mechanism for startups, helping a company create itself from nothing.