高级 #pl#lambda#theory

Lambda 演算基础

Lambda 演算(λ-calculus)是函数式编程的理论基础——只用三样东西(变量、抽象、应用)就能表达一切计算

🧬 计算的”最小 DNA”

你有没有想过一个问题:要表达一切计算,最少需要多少”基本元素”?

你需要循环吗?需要 if-else 吗?需要赋值吗?需要变量类型吗?

1930 年代,数学家 Alonzo Church 给出了一个惊人的答案:只需要三样东西——变量、函数定义、函数应用。

这就是 Lambda 演算(λ-calculus)——一个极其简洁但图灵完备的数学模型。它只用三条规则就表达了所有可能的计算。

🧱 类比:乐高的基础积木

乐高有成千上万种零件,但最基础的”2×4 积木块”只有一种形状。用足够多的基础积木,你可以拼出任何东西——城堡、飞船、恐龙……

Lambda 演算就是编程世界的”基础积木”——它本身很简单,但足以构建任何程序。现代编程语言(尤其是函数式语言)的所有特性,都能追溯到 Lambda 演算中的某个概念。


📐 Lambda 演算的三要素

Lambda 演算只有三种构造:

λ-calculus 语法:
⟨表达式⟩ := ⟨变量⟩          — 比如 x, y, z
         | ⟨λ⟩⟨变量⟩.⟨表达式⟩ — 函数定义(抽象)
         | ⟨表达式⟩ ⟨表达式⟩  — 函数应用

1️⃣ 变量(Variable)

x         — 名字,指代某个值
y

2️⃣ 抽象(Abstraction)——定义函数

λx. x    — 接收参数 x,返回 x(恒等函数)
λx. x+1  — 接收参数 x,返回 x+1

语法λ 后面跟参数名,. 后面跟函数体。

用 Python 来类比:

# λx. x        → lambda x: x
# λx. x+1      → lambda x: x+1
# λx. λy. x    → lambda x: lambda y: x

3️⃣ 应用(Application)——调用函数

(λx. x) y    — 把 y 传给恒等函数
(λx. x+1) 5  — 把 5 传给"加一"函数

🔄 β 归约——计算的本质

Lambda 演算中的”计算”就是做 β 归约(Beta Reduction)——把函数应用于参数,用参数替换函数体中的变量。

(λx. M) N  →  M[x := N]

把这个 N 代入 M 中所有的 x 出现的位置。

例子

(λx. x) apple            → apple        (恒等函数)
(λx. λy. x) a b          → a            (取第一个参数)
(λx. λy. y) a b          → b            (取第二个参数)

(λx. x * x) 5            → 5 * 5        → 25
(λf. f 3) (λx. x + 1)    → (λx. x+1) 3  → 3+1  → 4

多步归约

(λx. λy. x) (λz. z) (λw. w)
→ (λy. (λz. z)) (λw. w)    — 把 (λz.z) 代入 x
→ (λz. z)                   — 把 (λw.w) 代入 y(但 y 没在函数体中出现)

这就是计算的本质——反复做替换,直到不能再替换(得到范式)。


🎯 用 Lambda 演算”编码”数据

Lambda 演算没有数字、没有布尔值、没有 if-else。但这些概念都可以用函数”编码”出来:

Church 编码:布尔值

# True = λx. λy. x
TRUE = lambda x: lambda y: x

# False = λx. λy. y
FALSE = lambda x: lambda y: y

# if-then-else:接收一个条件、一个真分支、一个假分支
# if cond then t else f = cond t f
# 因为 True t f = t,False t f = f

IF = lambda cond: lambda t: lambda f: cond(t)(f)

# 测试
print(IF(TRUE)("yes")("no"))   # "yes"
print(IF(FALSE)("yes")("no"))  # "no"

为什么这就是布尔值? True 返回第一个参数,False 返回第二个参数——这就是”条件选择”的本质。

Church 编码:数字

# 数字 n = λf. λx. f(f(...f(x)...)) — 把 f 应用 n 次到 x
ZERO = lambda f: lambda x: x                # 应用 0 次
ONE = lambda f: lambda x: f(x)              # 应用 1 次
TWO = lambda f: lambda x: f(f(x))           # 应用 2 次
THREE = lambda f: lambda x: f(f(f(x)))      # 应用 3 次

# 后继:数字 + 1
SUCC = lambda n: lambda f: lambda x: f(n(f)(x))

print(SUCC(ONE)(lambda x: x+1)(0))  # 2 — 验证

💡 Church 编码的核心思想:数字 n 就是”把 f 应用 n 次”。加法就是”先应用 m 次 f,再应用 n 次 f”。


🔬 Lambda 演算为什么重要?

意义说明
计算的数学模型图灵机描述”硬件级”计算,λ演算描述”逻辑级”计算
函数式编程的根基函数式语言(Haskell、Scheme)直接基于 λ 演算设计
变量作用域λ 演算中的”绑定变量”和”自由变量”就是变量作用域的数学定义
类型系统简单类型 λ 演算(Simply Typed Lambda Calculus)是类型系统的起点
编程语言语义很多语言的语义通过”翻译成 λ 演算”来定义

自由变量 vs 绑定变量

λx. (x y)
     ↑   ↑
    绑定  自由

x 是绑定变量(由 λx 绑定)
y 是自由变量(没有被 λ 绑定)

联系作用域:这就是变量作用域的数学定义——绑定变量就是局部变量,自由变量就是捕获的外部变量。


🧠 递归——Y 组合子

在 Lambda 演算中,函数没有名字(都是匿名的)。那怎么实现递归——函数调用自己?

答案是用 Y 组合子(Y Combinator)——一个神奇的函数,可以在没有命名的系统中实现递归:

# Y = λf. (λx. f (x x)) (λx. f (x x))
Y = lambda f: (lambda x: f(x(x)))(lambda x: f(x(x)))

# 用 Y 实现阶乘
fact = Y(lambda f: lambda n: 1 if n == 0 else n * f(n-1))
print(fact(5))  # 120

💡 你不需要记住 Y 组合子的实现细节。它的意义在于证明了:即使没有”命名函数”的能力,递归仍然可以做得到。这也是 Lambda 演算图灵完备的关键证明。


📝 小结

概念一句话
λ 演算计算的数学模型——只用变量、抽象、应用
β 归约把参数代入函数体——计算的本质
Church 编码用函数表示布尔值、数字、数据结构
自由/绑定变量作用域的数学定义
Y 组合子匿名系统中实现递归的”魔法函数”
图灵完备λ 演算可以表达任何可计算函数

🎯 思考题(λx. x x) (λx. x x) 这个表达式归约会得到什么?(提示:这是一个”永恒循环”——每次归约后得到自身。它是 Lambda 演算中”无限循环”的表达。)

为什么先学这个? Lambda 演算是函数式编程的理论源泉。接下来看看基于函数式思想的数据结构——代数数据类型与模式匹配