函数式编程

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)            # → 8

Monad:带上下文的值

Monad 是 FP 里最被误解的概念。直觉是:带上下文的值

Monad上下文解决的问题
Option/Maybe值可能存在,也可能不存在空值安全,替代 null
Result/Either操作可能成功,也可能失败错误处理,替代异常
List零个或多个值非确定性计算
Promise/Future未来某时刻的值异步操作
IO执行 I/O 才能得到的值副作用隔离

Monad 的核心操作是 bind(也叫 flatMapand_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 提升成了编译时错误。

五、多语对照:不同哲学的工程落地

纯函数与副作用

维度PythonTypeScriptRust
纯函数强制无,靠约定无,靠约定借用检查器限制共享可变状态
副作用隔离无原生机制无原生机制所有权系统约束副作用范围
引用透明性开发者自律开发者自律类型系统部分保证

不可变性

维度PythonTypeScriptRust
不可变类型tuplefrozensetNamedTupleReadonly<T>as const默认不可变绑定(let
强制程度部分(容器内元素仍可变)编译时检查(可绕过)编译时强制,mut 显式标记
持久化结构无原生支持无原生支持无原生支持(需库)

高阶函数与组合

维度PythonTypeScriptRust
函数字面量lambda(单表达式限制)箭头函数 =>闭包 |x| x + 1
组合方式functools、推导式数组方法链、fp-ts pipe迭代器链(零成本抽象)
柯里化functools.partial箭头函数嵌套原生支持闭包嵌套

模式匹配与 ADT

维度PythonTypeScriptRust
模式匹配3.10+ match(基础)无原生(需 switch + 类型守卫)match 表达式(强大)
穷尽性检查不支持不支持编译器强制
和类型Union + Literal(模拟)discriminated unionenum(原生支持,可携带数据)

Monad 与错误处理

维度PythonTypeScriptRust
Option/MaybeOptional[T](类型提示)T | null、fp-ts OptionOption<T>(原生)
Result/Either异常为主fp-ts/Effect ResultResult<T, E>(原生)
链式调用无原生Promise.then、fp-ts pipeand_thenmap(原生)

三门语言的哲学倾向:

  • 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 维度