跳转到内容

编程范式

范式是组织程序的方式:命令式关注状态变化,函数式关注值的变换,面向对象关注消息与封装,逻辑式关注关系与搜索。本章比较它们的核心抽象、各自擅长的问题,以及现代语言的多范式融合。

范式核心抽象程序是……代表
命令式状态与语句改变状态的一系列指令C、Pascal
函数式值与函数表达式的求值Haskell、OCaml、Elixir
面向对象对象与消息相互发送消息的对象Smalltalk、Java、Simula
逻辑式关系与查询事实与规则的声明Prolog

范式不是语法风格,而是可推理性的来源:每种范式定义"程序正确"的推理方式。

命令式:程序 = 一系列改变状态的语句。推理方式是跟踪状态——执行到第 kk 行时,所有变量的值是什么?状态空间随程序增长,是命令式程序难以推理的根源。

函数式:程序 = 表达式求值。三个关键性质:

  • 不可变性:值不改变,"用新值代替旧值"。不可变性让并发推理变简单——没有共享可变状态,就没有数据竞争(→ 并发与同步);
  • 纯函数:相同输入恒有相同输出、无副作用——可测试、可缓存(记忆化)、可并行;
  • 高阶函数:函数作为参数与返回值——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 是它的现代用武之地
  • 现代语言是多范式的:判断标准是哪种组织让推理最局部