主题
第 3 章:名词王国与动词王国 —— 编程范式的兴衰
"To a man with a hammer, everything looks like a nail."
—— Abraham Maslow
1. 引言:哲学之战
编程范式(Paradigm)不仅仅是工具集,它们是思维的操作系统。 它们决定了你如何看待世界,如何解构问题。
在计算机科学的短暂历史中,爆发过无数次范式战争。最惨烈、最持久的,莫过于面向对象编程 (OOP) 与 函数式编程 (FP) 之间的战争。
这不仅仅是技术之争,这是古希腊哲学的延续:
- OOP (柏拉图主义):相信世界由永恒的理念 (Class) 构成。现实中的猫只是 "猫" 这个理念的一个投影(Instance)。OOP 关注主语(谁在做?)。
- FP (赫拉克利特主义):相信 "人不能两次踏进同一条河流"。世界是流动的、变化的。FP 关注谓语(发生了什么变换?)。
语言相对论 (Sapir-Whorf Hypothesis) 告诉我们:语言决定思维。 Java 程序员看到的是一个由名词(对象)组成的静态世界。 Haskell 程序员看到的是一个由动词(函数)组成的流动世界。
2. 名词王国的崛起:OOP 的黄金时代
2.1 起源:细胞与生物学
OOP 的发明者 Alan Kay 拥有生物学背景。他的初衷并不是创建复杂的类继承体系,而是模拟细胞 (Cells)。
- 每个细胞都是独立的。
- 细胞之间通过化学信号 (Messages) 通信。
- 细胞内部的状态对外不可见。
这就是 OOP 的核心三要素:封装 (Encapsulation)、消息传递 (Messaging)、动态绑定 (Dynamic Binding)。 请注意,Alan Kay 并没有强调 "继承"。
2.2 Java 的背叛:名词管理学
1990 年代,Java 的出现将 OOP 推向了神坛,但也将其庸俗化。 Java 强制要求一切皆对象。
- 你想写一个
main函数?不行,你必须先造一个Main类。 - 你想写一个数学公式
sin(x)?不行,你必须调用Math.sin(x)。
这导致了 Steve Yegge 所说的 "名词王国 (Kingdom of Nouns)"。 动词(行为)在 Java 中是二等公民。它们不能独立存在,必须依附于某个名词(类)。 于是我们看到了这种荒谬的命名: UserRequestProcessorFactoryBuilderImpl 这就像是为了喝水,你必须先创建一个 "喝水者",再创建一个 "杯子持有者",再创建一个 "吞咽动作执行者"。
2.3 猩猩与香蕉问题
Erlang 的创造者 Joe Armstrong 曾有名言:
"The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."
"面向对象语言的问题在于,它们总是带着隐式的环境。你只想要一根香蕉,但你得到的是一只拿着香蕉的猩猩,以及整片丛林。"
当你试图复用一个 User 类时,你发现它依赖了 Database,Database 依赖了 Config,Config 依赖了 Network... 最终你为了复用一行代码,不得不引入整个项目。
3. 动词王国的复兴:FP 的回归
函数式编程其实比 OOP 更古老(Lisp 诞生于 1958 年),但它长期被认为是学术界的玩具。 直到 2010 年代,随着多核 CPU 和 分布式计算 的普及,FP 突然王者归来。
3.1 Lambda 演算:数学的纯净
FP 建立在 Alonzo Church 的 Lambda 演算 之上。 在 Lambda 演算中,没有数字,没有布尔值,只有函数。
让我们用 JavaScript 模拟一下 Church Numerals(用函数表示数字),感受一下数学之美:
javascript
// 定义:0 是一个接受函数 f 和参数 x,直接返回 x 的函数
const ZERO = f => x => x;
// 定义:1 是应用一次 f
const ONE = f => x => f(x);
// 定义:2 是应用两次 f
const TWO = f => x => f(f(x));
// 定义:加法
const ADD = n => m => f => x => m(f)(n(f)(x));
// 验证:1 + 2 = 3
// 这不是魔法,这是数学。3.2 副作用:并发的噩梦
为什么 FP 赢得了现代战争?因为副作用 (Side Effects)。
在 OOP 中,对象的方法通常会修改内部状态(副作用)。 user.setBalance(100)
在单线程时代,这没问题。 但在多线程时代,如果两个线程同时调用 setBalance,就会发生竞态条件 (Race Condition)。为了防止出错,你必须加锁。锁会导致死锁,会导致性能下降。
FP 的答案: 如果数据是不可变 (Immutable) 的,如果函数是纯 (Pure) 的(没有副作用),那么根本不需要锁! const newBalance = calculateBalance(oldBalance, 100)
纯函数是引用透明 (Referentially Transparent) 的。这意味着你可以随时把 f(x) 替换为它的计算结果,而不会改变程序的行为。这使得并行计算和缓存变得极其简单。
3.3 Monad:可编程的分号
提到 FP,大家都会被 Monad(单子)吓跑。 "Monad 是自函子范畴上的幺半群。" —— 这句话除了装逼没有任何用处。
让我们用工程师听得懂的话解释:Monad 是一种设计模式,用于处理副作用(如空值、错误、IO)。
想象一下铁路 (Railway Oriented Programming):
- Happy Path (绿轨):一切正常的数据流。
- Error Path (红轨):发生错误的数据流。
在传统编程中,我们用 try-catch 或 if (err) 到处打补丁。 在 FP 中,我们用 Maybe 或 Either Monad 把这两条轨道封装起来。
javascript
// 伪代码:Monad 风格的错误处理
// 不需要 try-catch,数据在管道中流动
const result = getUser(id) // 返回 Maybe<User>
.map(user => user.address) // 如果 user 存在,取 address;否则传递空
.map(addr => addr.zipCode) // 如果 address 存在,取 zipCode
.getOrElse('00000'); // 如果中间任何一步失败,返回默认值Monad 就像是一个容器,它保护着里面的值,并处理所有的脏活累活(判空、异常捕获)。
4. 伟大的融合:多范式时代
历史并没有终结于 FP 取代 OOP。 相反,我们进入了混合范式 (Multi-Paradigm) 的时代。
4.1 React Hooks:代数效应
React 是一个迷人的怪胎。它用 FP 的理念(UI 是状态的函数 v = f(s))构建架构,但组件内部却需要管理状态。
React Hooks (useState, useEffect) 其实是 FP 中高深概念 代数效应 (Algebraic Effects) 的一种落地实现。 它允许纯函数组件 "暂停" 执行,向运行时环境 "请求" 一些状态或副作用,然后恢复执行。
4.2 Rust:实用主义的巅峰
Rust 是现代语言设计的集大成者。它完美融合了 OOP 和 FP:
- Data:
struct(类似 Class,但只是数据)。 - Behavior:
trait(类似 Interface,定义行为)。 - Logic:
enum(Sum Types,FP 的核心数据结构)。
Rust 抛弃了继承,拥抱了组合。它用所有权 (Ownership) 系统强制实现了内存安全,而不需要垃圾回收 (GC)。
4.3 表达式问题 (The Expression Problem)
这是一个经典的计算机科学问题: "如何在这个系统中添加新的数据类型或新的操作,而不需要重新编译现有的代码?"
- OOP:添加新数据类型很容易(加个子类),但添加新操作很麻烦(要改所有类)。
- FP:添加新操作很容易(加个函数),但添加新数据类型很麻烦(要改所有函数)。
现代语言(如 Scala, Swift, Rust)都在尝试通过 Type Classes 或 Extensions 来同时解决这两个问题,打破 OOP 和 FP 的二元对立。
5. 结语:超越范式
编程的终极目标不是写出符合 OOP 或 FP 教条的代码。 编程的目标是控制复杂度。
- 宏观上使用 OOP:在设计微服务、模块边界、领域模型时,OOP 的"封装"思想依然强大。它能帮我们划分边界,隐藏细节。
- 微观上使用 FP:在编写具体的业务逻辑、数据处理管道时,FP 的"纯函数"和"不可变性"能极大减少 Bug,提高可读性。
当你不再纠结于"这是不是纯粹的 FP"时,你就真正理解了范式。 你手里拿的不再是锤子,而是整个工具箱。你可以在名词王国和动词王国之间自由穿梭,成为一名真正的全栈语言学家。