从纯粹的计算理论视角看,人类似乎只需要一种编程语言。
在图灵机的宇宙中,计算不过是无限纸带上的符号读写与状态迁移;在 $\lambda$ 演算的世界里,万物皆是匿名函数的嵌套与无休止的归约。一旦一门语言跨越了图灵完备的门槛,在表达能力上,它便足以描述任何可计算的过程。如果最原始的符号替换已经足以表达一切,那么为什么在过去半个多世纪里,人类仍不断发明新的编程语言?
答案并不在于计算能力的提升,而在于形态的诞生。未经驯化的纯粹计算,对人类的心智而言是一场灾难。编程语言的发展史,绝不是单纯堆砌语法特性的历史,而是一部人类试图给无形的计算法则赋予形态的历史。
本书试图从这个角度重新审视编程语言。它既不是一本教授某种具体语言语法的教程,也不是一部沉迷于形式推导的纯理论作品。相反,本书将结合递归论、语义学与类型论的视角,把那些看似零散的语言特性重新拆解,并回答一个更基础的问题:
计算是如何逐渐获得形态的?
为了回答这个问题,我们将沿着计算获得形态的四个维度展开。
第一卷:结构——计算的语法形态
计算最初的形态来自对符号的组织。程序在表面上只是线性的字符序列,但在语法规则的作用下,这些字符被折叠为具有层级关系的结构——抽象语法树。作用域规则进一步为名称划定边界,使符号不再是随意漂浮的标记,而成为在特定空间内具有意义的引用。最后,通过操作语义,程序的执行被刻画为一系列可以推导的状态转移。
在这一阶段,计算第一次获得了清晰的轮廓:它不再只是符号替换,而是一种可以被结构化描述的过程。
第二卷:约束——类型化计算
一旦计算获得表达能力,它很快便会陷入混沌。任意结构之间的自由组合,使得错误往往只能在运行时显现。类型系统正是在这样的背景下出现,它为计算建立起一套逻辑骨架。乘积类型与和类型构成了建模现实世界状态的基本形态;参数多态与子类型则揭示了不同抽象哲学之间的分歧。当类型系统被推进到极致,在 Curry–Howard 对应的视角下,程序甚至可以被理解为数学证明。
计算在这一阶段不再只是可执行的过程,而成为可以被逻辑约束和验证的对象。
第三卷:效应——不纯计算
纯粹的数学计算最终必须落入现实世界。程序需要占据内存、经历时间,并与外部环境乃至其他计算实体交互。异常处理、协程与 async/await 看似截然不同,但在更深层的理论中,它们可以统一为对“未来执行”的操纵;内存管理则在垃圾回收与线性逻辑之间呈现出截然不同的哲学选择;而在并发系统中,程序不再只是单一的执行轨迹,而是一组相互通信的计算实体。
计算在这一阶段获得了时间、空间与资源的维度。
第四卷:组织——大规模计算
当程序规模从数十行增长到数百万行时,新的问题随之出现:复杂性如何被组织?模块系统与信息隐藏成为划分边界的工具,分发机制决定了行为如何在不同数据形态之间展开,而跨越进程乃至跨越机器的边界时,类型系统与契约又承担起新的防御角色。
计算在这里不再只是个体程序的执行,而是一种能够支撑大型软件系统持续演化的制度结构。
过去几十年间,编程语言理论已经形成了一套成熟的教材体系,本书的形成亦深受它们的影响:
本书并不试图重复这些经典,而是试图关注一个稍有不同的问题:在自由、规则与真理之间,计算是如何获得“形态”的?
每一种编程语言,本质上都是在这些维度中做出的一组设计取舍。C++ 通过模板单态化在编译期展开泛型,牺牲编译时间以获得接近零成本抽象的运行时性能;Java 则采用类型擦除放弃了泛型的运行时实体,在保持虚拟机向后兼容的前提下引入泛型机制;Rust 通过区域推导与所有权模型,将内存安全问题转化为静态类型系统中的可验证约束;而 Lisp 的同像性设计,则允许程序直接操作自身的语法结构,使语言具备极强的元编程能力。
从这个视角看,编程语言之间的差异,并不是语法风格的不同,而是对计算结构进行组织与约束方式的不同。每一种语言,都在为计算提供一种可能的形态。
当你合上这本书时,希望你再看任何一门编程语言时,看到的不再是零散的语法特性,而是它在计算空间中的一种结构形态。
Programming languages shape computation the way morphology shapes form in nature.