函数式编程
OOP 把状态交给对象负责——每块数据都有守护者。这解决了”数据裸露”的问题,但没有解决更深的问题:状态还在,可变性还在,时间还在流动。
account = BankAccount(1000)
account.withdraw(200) # 状态变了
account.deposit(500) # 又变了
# account 现在的值,取决于操作的历史顺序你必须追踪这个对象的整个历史才能理解它现在是什么。在并发环境下,两个线程同时修改同一个对象,历史顺序不可预测——这是 OOP 封装边界无法防御的。
FP 的回答更激进:把时间从计算的核心里驱逐出去。不管理可变状态——消除它。用”输入→输出的永恒变换”替代”在某个时刻修改某个状态”。
一、函数是什么
程序是函数的复合
在命令式编程里,程序是一系列步骤:先做 A,再做 B,然后做 C。步骤有顺序,有时间,有中间状态。
在 FP 里,程序是函数的复合:
result = f(g(h(input)))没有”先做什么再做什么”——只有”输入经过 h 变换,结果再经过 g 变换,最终经过 f 变换得到输出”。这是数学的语言,不是机器的语言。
Lambda 演算:函数是计算的最小单元
Alonzo Church 在 1936 年提出 Lambda 演算——比第一台电子计算机还早十年。它极其简洁:
<expression> ::= <variable>
| λ<variable>.<expression> -- 定义一个函数
| (<expression> <expression>) -- 应用一个函数就这三条规则,可以表达所有可计算的东西。Church 证明,Lambda 演算和图灵机在计算能力上完全等价——这是 Church-Turing 论题。
这意味着 FP 不是命令式编程的受限子集,而是等价的另一种计算视角:函数应用,而不是状态修改。
函数是一等公民
FP 要求函数是一等公民(First-class citizen):函数可以像数值一样被传递、返回、存储。
def apply_twice(f, x):
return f(f(x))
apply_twice(lambda x: x + 1, 5) # → 7这不只是语法便利——它意味着”变换本身”可以被抽象、组合、参数化。程序从”对数据做什么”变成”用什么样的变换处理数据”。
二、引用透明性:让程序可推理
引用透明性:程序变成方程组
纯函数的核心性质叫引用透明性(Referential Transparency):
一个表达式,如果可以用它的值替换而不改变程序行为,就是引用透明的。
def double(x):
return x * 2
# double(3) 可以直接替换成 6,程序行为不变
result = double(3) + double(3) # 等价于 6 + 6 = 12这让你可以像做代数一样推理程序:把表达式替换成它的值,验证等式。这是 FP 最强大的认知工具。
对比不纯的函数:
counter = 0
def increment():
global counter
counter += 1
return counter
x = increment() # 1
y = increment() # 2 —— 不能用 x 替换 y,它们不相等increment() 的结果依赖调用历史,无法替换,无法推理——这就是时间带来的认知负担。
纯函数的三个推论
纯函数(相同输入必然相同输出,无可观察副作用)带来三个直接推论:
- 可测试:无需 mock 外部依赖,输入输出完全确定
- 可缓存:相同输入→相同输出,可以安全地记忆化(memoization)
- 可并行:无共享状态,多线程调用永远安全
这三件事在命令式程序里都需要额外的工作量;在 FP 里是默认免费的。
副作用的精确定义
| 类型 | 示例 |
|---|---|
| I/O 副作用 | 读写文件、网络请求、数据库查询 |
| 状态修改 | 修改全局变量、修改对象属性 |
| 异常 | 抛出异常(中断正常控制流) |
| 非确定性 | 随机数、当前时间、线程调度 |
这些都是”结果依赖时间”的体现——同样的调用,在不同时刻得到不同结果。FP 的目标不是禁止这些,而是把它们推到程序的边缘,让核心逻辑保持纯净。
三、拒绝时间的流动
可变性是时间的体现
为什么并发程序难以推理?因为同一块内存在时间轴上有不同的值。线程 A 读到的值,和线程 B 随后写入的值,是不同时刻的同一个地址——时间制造了不确定性。
FP 的答案是:消除可变性,就是消除程序的历史依赖。
# 命令式:修改列表——有历史
items = [1, 2, 3]
items.append(4) # items 被改变了,它有了"过去"
# 函数式:产生新值——无历史
items = [1, 2, 3]
new_items = items + [4] # items 永远是 [1, 2, 3],new_items 是全新的旧的 items 永远不变。没有”之前是什么”,只有”这个变换产生了什么”。
不可变性:变化是新值的诞生
不可变性的哲学立场是:变化不是旧值被修改,而是新值被创造。
这在前端状态管理 Redux 里有直观的体现:每次用户操作不修改现有状态,而是产生一个新的状态快照。所有历史快照都保留。
这带来了一个免费的能力:时间旅行调试——你可以在任意历史时刻检查程序状态,甚至撤销、重放操作。这不是魔法,是不可变性的直接推论。
代价:持久化数据结构需要”结构共享”来避免每次复制整个数据;大量短生命周期对象会增加 GC 压力;某些原地算法(排序、矩阵运算)在不可变世界里需要特殊处理。
副作用无法消除,只能隔离
FP 不能消除 I/O——程序必须和外部世界交互。Haskell 的答案是 Monad,实践中演变为一种架构模式:
┌─────────────────────────────────────┐
│ Functional Core(纯函数核心) │
│ · 业务逻辑 │
│ · 数据变换 │
│ · 完全可测试 │
└──────────────┬──────────────────────┘
│ 薄薄的边界层
┌──────────────▼──────────────────────┐
│ Imperative Shell(命令式外壳) │
│ · 读写数据库 │
│ · 网络请求 │
│ · 用户输入 │
└─────────────────────────────────────┘React 是这个模式的大规模实践:组件(纯函数,输入 props,输出 UI)是核心;事件处理、网络请求是外壳。
四、从函数到系统
高阶函数:变换本身可以被变换
高阶函数(Higher-Order Function)接受函数作为参数,或返回函数作为结果。这让”变换”本身成为可操作的对象:
# map:把"对单个元素的变换"扩展成"对整个列表的变换"
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers)) # [2, 4, 6, 8, 10]
# filter:把"判断条件"变成"筛选器"
evens = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]你不需要写循环、管理索引、维护累积变量——你描述变换是什么,而不是怎么执行变换。
函数组合:小变换串成大变换
函数组合(f ∘ g)把多个小函数串联成一个大函数:
from functools import reduce
# 管道:数据从左流向右
def pipe(*fns):
return lambda x: reduce(lambda v, f: f(v), fns, x)
process = pipe(
lambda x: x * 2, # 翻倍
lambda x: x + 1, # 加一
lambda x: str(x), # 转字符串
)
process(5) # "11"每个函数只做一件事,组合起来完成复杂逻辑。这和 Unix 管道的哲学一致:cat file | grep pattern | sort | uniq。
柯里化(Currying):把多参数函数转化为一系列单参数函数,使部分应用成为可能:
# add(x, y) 柯里化后
add = lambda x: lambda y: x + y
add5 = add(5) # 固定第一个参数,得到新函数
add5(3) # → 8Monad:带上下文的值
Monad 是 FP 里最被误解的概念。直觉是:带上下文的值。
| Monad | 上下文 | 解决的问题 |
|---|---|---|
Option/Maybe | 值可能存在,也可能不存在 | 空值安全,替代 null |
Result/Either | 操作可能成功,也可能失败 | 错误处理,替代异常 |
List | 零个或多个值 | 非确定性计算 |
Promise/Future | 未来某时刻的值 | 异步操作 |
IO | 执行 I/O 才能得到的值 | 副作用隔离 |
Monad 的核心操作是 bind(也叫 flatMap、and_then):把”带上下文的值”传给一个函数,函数返回”带上下文的新值”,自动处理上下文传递:
// Rust:Result monad 链式调用
let result = parse_input(raw)
.and_then(|n| validate(n)) // 任意一步 Err,后续自动跳过
.and_then(|n| compute(n))
.map(|n| format!("Result: {}", n));不需要每步都 if err != nil——上下文(是否出错)自动沿链传播。
代数数据类型:精确建模数据
代数数据类型(ADT)是 FP 的类型基石:
| 类型 | 语义 | 示例 |
|---|---|---|
| 积类型(Product) | 所有字段同时存在(AND) | Point { x: f64, y: f64 } |
| 和类型(Sum) | 只能是其中之一(OR) | Shape = Circle | Rectangle | Triangle |
配合模式匹配,编译器检查是否处理了所有情况(穷尽性检查):
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
// 若遗漏 Triangle,编译器报错,不是运行时崩溃
}
}这把”忘记处理某种情况”从运行时 bug 提升成了编译时错误。
五、多语对照:不同哲学的工程落地
纯函数与副作用
| 维度 | Python | TypeScript | Rust |
|---|---|---|---|
| 纯函数强制 | 无,靠约定 | 无,靠约定 | 借用检查器限制共享可变状态 |
| 副作用隔离 | 无原生机制 | 无原生机制 | 所有权系统约束副作用范围 |
| 引用透明性 | 开发者自律 | 开发者自律 | 类型系统部分保证 |
不可变性
| 维度 | Python | TypeScript | Rust |
|---|---|---|---|
| 不可变类型 | tuple、frozenset、NamedTuple | Readonly<T>、as const | 默认不可变绑定(let) |
| 强制程度 | 部分(容器内元素仍可变) | 编译时检查(可绕过) | 编译时强制,mut 显式标记 |
| 持久化结构 | 无原生支持 | 无原生支持 | 无原生支持(需库) |
高阶函数与组合
| 维度 | Python | TypeScript | Rust |
|---|---|---|---|
| 函数字面量 | lambda(单表达式限制) | 箭头函数 => | 闭包 |x| x + 1 |
| 组合方式 | functools、推导式 | 数组方法链、fp-ts pipe | 迭代器链(零成本抽象) |
| 柯里化 | functools.partial | 箭头函数嵌套原生支持 | 闭包嵌套 |
模式匹配与 ADT
| 维度 | Python | TypeScript | Rust |
|---|---|---|---|
| 模式匹配 | 3.10+ match(基础) | 无原生(需 switch + 类型守卫) | match 表达式(强大) |
| 穷尽性检查 | 不支持 | 不支持 | 编译器强制 |
| 和类型 | Union + Literal(模拟) | discriminated union | enum(原生支持,可携带数据) |
Monad 与错误处理
| 维度 | Python | TypeScript | Rust |
|---|---|---|---|
| Option/Maybe | Optional[T](类型提示) | T | null、fp-ts Option | Option<T>(原生) |
| Result/Either | 异常为主 | fp-ts/Effect Result | Result<T, E>(原生) |
| 链式调用 | 无原生 | Promise.then、fp-ts pipe | and_then、map(原生) |
三门语言的哲学倾向:
- Python:实用主义。FP 特性作为工具箱存在——
map/filter/reduce随时可用,但不强迫你用。生成器是最接近惰性求值的原生支持。 - TypeScript:生态驱动。原生缺乏 Monad 支持,fp-ts/Effect 把 Haskell 的类型论引入 JavaScript 生态。结构化类型使函数组合更灵活。
- Rust:零成本 FP。迭代器链编译后等同于手写循环,
Option/Result是语言核心,借用检查器天然抑制共享可变状态——FP 原则被编译进了语言设计。
延伸阅读
- Hughes, John. “Why Functional Programming Matters.” (1990) — FP 经典论文
- Pierce, Benjamin. Types and Programming Languages. MIT Press, (2002) — Lambda 演算理论
- Hickey, Rich. “Simple Made Easy.” (2011, Strange Loop 演讲) — 不可变性与复杂度
- Rust Book Ch13 — 迭代器与闭包
关联 meta 维度
- 01 面向对象编程 — OOP 和 FP 的对立:管理状态 vs 消除状态
- 04 泛型编程 — FP 和泛型经常一起出现
- meta: 类型系统 — 代数数据类型是 FP 的类型基石
- meta: 错误处理 — Result/Either 的 FP 错误处理哲学