编程范式
范式是组织程序的方式:命令式关注状态变化,函数式关注值的变换,面向对象关注消息与封装,逻辑式关注关系与搜索。本章比较它们的核心抽象、各自擅长的问题,以及现代语言的多范式融合。
| 范式 | 核心抽象 | 程序是…… | 代表 |
|---|---|---|---|
| 命令式 | 状态与语句 | 改变状态的一系列指令 | C、Pascal |
| 函数式 | 值与函数 | 表达式的求值 | Haskell、OCaml、Elixir |
| 面向对象 | 对象与消息 | 相互发送消息的对象 | Smalltalk、Java、Simula |
| 逻辑式 | 关系与查询 | 事实与规则的声明 | Prolog |
范式不是语法风格,而是可推理性的来源:每种范式定义"程序正确"的推理方式。
命令式与函数式
Section titled “命令式与函数式”命令式:程序 = 一系列改变状态的语句。推理方式是跟踪状态——执行到第 行时,所有变量的值是什么?状态空间随程序增长,是命令式程序难以推理的根源。
函数式:程序 = 表达式求值。三个关键性质:
- 不可变性:值不改变,"用新值代替旧值"。不可变性让并发推理变简单——没有共享可变状态,就没有数据竞争(→ 并发与同步);
- 纯函数:相同输入恒有相同输出、无副作用——可测试、可缓存(记忆化)、可并行;
- 高阶函数:函数作为参数与返回值——map/filter/reduce 是它的词汇。
函数式的代价:改变大量状态时(编辑器、游戏),"用新值代替旧值"需要持久化数据结构(结构共享)或借用可变状态。现实语言在两者之间:Rust 允许可变但默认不可变;Swift 的值类型与 COW;Clojure 的持久化集合。
三个经典机制:封装(状态与操作绑定)、继承(复用与子类型)、动态分派(方法按对象运行时类型选择)。
设计经验澄清了它们的地位:面向对象的核心是动态分派而非继承——多态与接口替换才是可扩展性的来源,继承复用反而造成脆弱基类问题(→ 设计原则与模式)。"组合优于继承"是这条经验的口号。动态分派的实现是虚表(vtable):每个类一张方法表,对象持有指向它虚表的指针——O(1) 的间接跳转(→ 指令集架构)。
程序 = 事实与规则(Prolog)。求值即搜索:统一(unification)+ 回溯,"数据库的反向查询"。小众但在特定领域无可替代:约束求解、专家系统、Datalog(数据库递归查询与程序分析)。
大多数现代语言都是多范式的,各有侧重:
- Rust:函数式(迭代器、模式匹配、代数数据类型)+ 系统级(所有权、无 GC)+ 面向对象(trait 与动态分派);
- Scala/Kotlin:OOP + 函数式;
- Python/JS:命令式为主,吸收函数式与 OOP 词汇;
- Go:有意反 OOP(无继承、组合 + 接口)。
融合的代价是选择困难:同一问题有四种写法。工程判断的标准不是范式纯洁性,而是哪种组织方式让这个问题的推理最局部——并发多的程序偏不可变,交互多的程序偏对象,算法多的偏函数式与纯数据。
- 范式是组织程序的方式,定义"程序正确"的推理方式
- 不可变性让并发推理变简单;纯函数可测试、可缓存、可并行
- 面向对象的核心是动态分派而非继承;组合优于继承,虚表是分派的实现
- 逻辑式 = 关系与搜索;约束求解与 Datalog 是它的现代用武之地
- 现代语言是多范式的:判断标准是哪种组织让推理最局部