May 2026
理解编程的更多方式
除了面向过程和面向对象,现在发展到哪了
Last Updated: 2026-05-09
摘要
学校里的编程语言入门,常常从面向过程和面向对象开始,最多再补一句函数式编程。但继续往后看,会发现编程语言和程序系统的发展远不止这些分类。AST、类型系统、IR、Effect、Actor、KPN、BOC 这些概念来自不同领域,却都在做相似的事:把原本靠程序员经验维持的结构显化出来,让系统能够理解、检查、调度、优化和恢复。
这篇文章不是完整的 PL 综述,而是一条个人学习路径:从学校里的语言分类,到编译器里的结构表示,再到并发模型、数据流、失败恢复和 AI Agent 时代的重新相遇。它想回答的问题是:除了面向过程和面向对象,程序还能被理解成什么?
学校里的那张小地图
入门分类
我上学时接触编程语言,最常见的分类是面向过程和面向对象。面向过程大概对应 C 这种语言:程序是一串步骤,函数把步骤分块,变量保存中间状态。面向对象则对应 C++、Java 这类语言:程序被组织成对象,对象有状态,也有方法,程序运行时就是对象之间互相调用。
有些教材会在后面再补一句:除了面向过程和面向对象,还有函数式编程。但通常也只是补一句。于是很多人早期形成的语言地图,大概就是过程、对象、函数这几块。
这张地图没有错。它对入门很有帮助,也能解释很多常见语言的差异。但后来继续往下学,会发现它太小了。编程语言和程序系统的发展,并不只是“过程 vs 对象”,也不只是多一个“函数式”标签。很多重要变化发生在更深的一层:程序中原本靠程序员经验、约定和手工控制维持的结构,逐渐被语言、框架和 runtime 表达出来,变成系统可以理解、检查、调度、优化和恢复的对象。
“显化”这个关键词
我这里想用一个词概括这个过程:显化。
显化不是简单地“写得更明确”。它指的是:原本隐含在程序员脑子里的东西,被提升成语言或系统的一部分。比如,类型系统把“这个值应该是什么形状”显化出来;dataflow 把“谁依赖谁”显化出来;actor 把“状态归谁管理”显化出来;checkpoint 把“失败后从哪里恢复”显化出来。
我的学习路径
我对这件事的理解,也是慢慢建立起来的。上学时用 C++ 和 LLVM 写过一个 COOL 语言的编译器,第一次知道源代码不是直接变成机器码,中间还有 AST、类型检查和 IR。后来学大数据和 Flink,才意识到程序不一定是顺序执行,也可以被看成数据流图。最近再看 BOC、FP、PL、Agent runtime,又发现并发、资源、失败、恢复、副作用这些东西,都可以被设计成新的程序模型。
所以这篇文章不是想重新给编程语言做完整分类,而是想记录一条个人学习路径:编程语言除了面向过程和面向对象,后来到底还在发展什么?
我的答案是:它一直在把各种隐含结构显化。
第一层显化:程序不只是指令流
从机器指令开始
如果从机器的角度看程序,最直接的模型是指令流。汇编语言很接近这个模型:一条条 instruction,操作寄存器,读写内存,通过 label 和 jump 改变控制流。程序当然可以有子程序和调用约定,但从机器视角看,它主要还是线性指令加跳转。
C 比汇编高级很多。它有函数、变量、结构体、if、while、for。程序员不用直接写寄存器和跳转,也不用每次都手动管理底层指令。但 C 的心智模型仍然很接近顺序机器:程序一步步执行,变量对应内存里的值,指针可以直接触碰地址,函数调用会压栈和返回。
高级语言真正带来的变化,不只是语法更舒服,而是程序开始有了更高层的结构。
AST:语法结构
比如一段 if 语句,在文本里只是字符序列;在机器码里可能是比较、跳转和基本块;但在编译器眼里,它首先会被解析成一棵 AST。AST 把源代码中的嵌套结构显化出来:条件是什么,then 分支是什么,else 分支是什么,表达式如何组合,函数定义在哪里,变量声明属于哪个作用域。
这就是第一层变化:程序不再只是文本,也不只是指令流,而是树状结构。
Type System 与 IR
接着是类型系统。汇编里没有高级类型,只有字节、寄存器、地址,以及指令如何解释这些位。高级语言引入类型以后,程序多了一层约束:这个变量是整数,那个函数接收字符串,这个对象有某些字段,这个表达式的返回值必须符合某种形状。
类型系统显化的是合法性约束。它让一部分错误可以在运行前被发现,而不是等程序跑到某一行才崩。
再往后是 IR。AST 很适合表达“程序员怎么写”,但不一定适合优化和生成机器码。编译器通常会把 AST 转成更适合分析和变换的中间表示。比如 LLVM IR、SSA 形式、控制流图、数据流信息,都在帮助编译器理解程序如何流动、值从哪里来、哪些操作可以优化。
第二层显化:组合不只是函数调用
函数调用的边界
过程式语言里,最自然的组合方式是函数调用。A 调 B,B 调 C。把重复逻辑抽成函数,把复杂流程拆成多个函数,然后通过调用关系组合起来。
这个模型非常基础,也非常强大。大多数工程代码都是这么组织的。但当系统复杂以后,函数调用本身也会暴露出问题。
一个函数表面上可能是:
A -> B
但真实情况经常不是这么简单。它可能读文件,写数据库,发网络请求,修改全局状态,抛异常,依赖上下文,或者在失败时需要重试。也就是说,函数调用背后经常藏着状态、失败、副作用和环境。
函数式编程的重新建模
函数式编程对这件事有另一种看法。它不只是“换一种语法写代码”,而是重新审视计算如何组合。纯函数的理想模型是:同样输入,总是得到同样输出,并且不偷偷修改外部世界。这样一来,函数就更容易组合、测试和推理。
高阶函数进一步把函数本身变成可以传递和组合的单元。map、filter、reduce 这些概念,本质上是在告诉程序员:你不一定要手写每一步循环,可以把“对每个元素做什么”作为一个可组合的计算传进去。
Effect:副作用进入结构
再往后,组合会遇到更复杂的问题。如果一个步骤可能失败,或者会访问外部世界,它就不再是简单的 A -> B,而更像:
A -> Effect[B]
这里的 Effect 可以代表很多东西:可能失败,可能异步,可能需要环境,可能访问文件、网络、数据库,也可能带着日志、上下文或取消语义。
Scala FP 生态里的 ZIO 是一个典型例子。它把一个 effectful program 表达成:
ZIO[R, E, A]
这里的 R 表示这个计算需要什么环境或依赖,E 表示它可能失败成什么错误,A 表示成功时产生什么结果。
Applicative、Monad、Effect 这些词听起来很抽象,但从工程角度看,它们在回答一个问题:当步骤不再是普通函数,而是带着上下文、失败和副作用的计算时,它们还能不能被组合?如果能,组合规则是什么?
这就是第二层显化:组合不只是函数调用。调用背后的上下文、失败、副作用,也需要被表达出来。
函数式编程显化了计算组合。Effect 系统显化了副作用边界。Monad / Applicative 这类抽象显化了带上下文计算的组合规则。
所以函数式编程并不是学校教材里“除了过程和对象,还有一种写法”那么简单。它背后关心的是:如何让程序更可组合、更可推理,如何把隐藏在调用链里的状态和副作用显化出来。
第三层显化:程序不一定顺序执行
顺序模型的边界
过程式程序默认是顺序执行:
A -> B -> C
程序员写出精确顺序,机器按照顺序执行。这个模型简单、直观,也很接近早期单机程序的运行方式。
但现实系统很快就会遇到顺序执行不够用的情况。服务器要同时处理多个请求,后台任务要并行运行,大数据系统要持续消费流,UI 要响应用户事件,分布式系统里多个节点要协作。程序不一定只有一条执行线。
线程和锁:最直接的答案
最直接的并发模型是线程和锁。多个线程共享内存,用锁保护共享资源。这个模型非常通用,也非常底层。它能解决并发执行的问题,但把大量正确性压力交给程序员:什么时候加锁,锁粒度多大,会不会死锁,会不会竞态,内存可见性如何保证。
上学期间学并发编程,很大程度上就是在学这些原语:线程、锁、条件变量、信号量、临界区、死锁、竞态。它们很重要,因为它们是很多系统的底层基础。但如果只停在这一层,就会觉得并发的核心问题都是“怎么正确用锁”。后来再看 Actor、CSP、Dataflow、BOC,才会发现还有另一条路:不是让程序员手工维护所有并发约束,而是把其中一部分约束提升成模型。
后来出现了很多并发模型,本质上是在把不同的并发约束显化出来。
Actor 与 CSP:状态和通信
Actor 模型的思路是:不要让大家随便共享状态。每个 actor 管理自己的状态,外部通过消息和它交互。这样状态所有权被显化了。你不再需要到处想“这个变量会不会被另一个线程同时改”,因为状态属于某个 actor,消息进入它的 mailbox,由它自己处理。
CSP 强调的是通信。并发实体通过 channel 交换数据,程序结构围绕通信过程组织。相比共享内存加锁,它把“谁和谁通信、通过什么通道通信”显化出来。Go 的 channel 就有这种味道。
Dataflow / KPN:依赖和调度
Dataflow / KPN 又是另一种视角。它不强调“先执行哪一行”,而强调“哪个节点依赖哪些数据”。当输入数据 ready,节点就可以执行。程序不再只是步骤列表,而是依赖图。KPN 里,process 通过 FIFO channel 通信;dataflow 系统里,operator 形成图,数据沿着边流动。
Flink 是这个方向的工程化例子。它不是一门 PL 语言,但它把 dataflow、state、checkpoint、backpressure 等执行语义做成了工业系统。在 Flink 里,程序员写的不是“第一秒做什么、第二秒做什么”,而是定义一个持续运行的数据流拓扑。runtime 负责调度、并行、状态管理和恢复。
BOC:资源占用意图
BOC 则显化了另一种东西:资源占用意图。它不是让程序员手写锁,而是让 behavior 声明自己需要哪些 Cown。runtime 根据资源冲突来调度行为。这样一来,“这段计算需要独占哪些资源”不再藏在锁操作里,而是成为 runtime 可以理解的结构。
从线程锁到 Actor、CSP、Dataflow、BOC,变化的不是并发方式变多了,而是并发中的状态、通信、依赖和资源占用逐渐被显化。
这也是我在看 Flink 和 BOC 时感觉很多系统联系起来的原因。它们表面属于不同领域:一个是大数据流处理,一个是并发模型研究。但它们都在做同一件事:把顺序程序里靠人脑维护的调度意图,交给更明确的程序模型和 runtime。
第四层显化:失败和恢复不该散落在 if / try 里
局部错误处理的边界
普通程序里,失败处理通常是局部的。函数返回错误码,调用者判断一下;或者抛异常,上层 try / except;失败了 retry,最后 cleanup。
这种方式在短函数和小程序里够用。但系统一旦变成长流程、分布式系统、流处理系统,失败就不再是局部问题。
一个任务可能运行很久,中途调用多个外部系统,持有状态,产生副作用。失败以后,真正的问题不是“catch 一下异常”,而是:已经完成的步骤算不算数?哪些动作可以重放?哪些动作不能重放?状态恢复到哪里?如果外部系统已经被调用,是否需要补偿?
从 supervision 到 durable execution
Erlang 的 supervision tree 给出了一种思路:失败不是意外,而是系统设计的一部分。进程可以崩,监督者负责按策略重启。这里显化的是失败管理结构:谁监督谁,失败后怎么处理,不再散落在每个业务函数里。
Flink 的 checkpoint 解决的是流处理里的状态恢复。流处理任务持续运行,状态不断变化。如果某个节点失败,系统需要从一致的 checkpoint 恢复,而不是从头乱跑一遍。checkpoint 显化的是恢复点。
Temporal(一个持久工作流 / 持久执行系统)这类 durable execution 系统则把长流程的 retry、timeout、replay、补偿变成 runtime 语义。程序员描述 workflow,runtime 记录历史,失败后可以重放到安全位置继续执行。
这些系统关心的细节不同,但背后的方向一致:失败和恢复不应该只是散落在 if、try、retry、rollback 里的补丁。
第四层显化的是失败路径。失败不再只是异常分支,而是程序模型和 runtime 语义的一部分。
第五层显化:变化和优化也可以被建模
手工优化的边界
优化最开始常常是手工技巧。这个结果能不能缓存?这段逻辑能不能跳过?多个任务能不能并行?某些结果能不能复用?这些判断往往依赖程序员对业务逻辑的理解。
问题在于,当系统变大以后,“什么变了”和“谁依赖它”会变得很难维护。依赖关系藏在调用链、配置、文件、状态和数据库里。程序员想优化,就必须先在脑子里恢复出一张隐形依赖图。
依赖、身份和变化
编译器优化的前提,是程序已经被转成可分析结构。只有当编译器知道控制流、数据流、值的依赖关系,才可能做常量折叠、死代码消除、内联、循环优化等变换。
Build system 也是类似问题。Make、Bazel 这类系统关心的是:哪些文件依赖哪些文件,哪些目标依赖哪些目标。如果输入没有变化,就不用重建。这里显化的是构建依赖。
前端里的 React reconciliation 也是一个很好的例子。列表渲染时,如果没有 key,系统很难判断哪个元素和之前的哪个元素是同一个。key 把身份显化出来,系统才能决定哪些组件可以复用,哪些需要重新创建。
Incremental computation 更进一步:它关心的不是每次重新算一遍,而是只重算受影响的部分。Differential dataflow 则把变化本身作为一等对象传播,而不是反复处理全量状态。
这些方向看起来分散:编译器、构建系统、前端框架、流处理。但它们都在处理同一个问题:如何显化变化、依赖和复用关系。
当系统知道什么变了、谁依赖它、哪些结果仍然有效,就能自动决定什么需要重算,什么可以跳过,什么可以复用。
第五层显化的是变化本身。优化不再只是程序员手写技巧,而是系统理解程序结构之后自动做出的决策。
补充:正确性也可以被显化
从测试到规则和证明
前面几层讨论的是结构、组合、并发、恢复和优化。还有一个更进一步的方向:正确性。
通常我们依赖测试、经验和 code review 来建立信心。但测试只能覆盖有限情况,经验也可能出错。于是 PL 里有一些方向尝试把规则、约束和证明也变成机器可检查的结构。
Logic programming 的思路是:不写一步步执行的指令,而是写规则和约束。Prolog 是经典代表。Datalog 则可以用于查询、权限、静态分析等场景。它们显化的是规则和推理关系。
Proof assistant 和 dependent type 方向走得更远。它们尝试把程序和证明联系起来,让某些性质不只是“测试过应该没问题”,而是可以被形式化表达和检查。
这部分我现在还不适合展开太多。它更像是这条线的补充:如果类型系统显化的是“哪些程序合法”,那么逻辑和证明方向进一步尝试显化“为什么这个程序是对的”。
AI 时代的小尾巴
Agent 问题可以回到历史中找答案
这篇文章的主线不是 AI。但 AI 时代让这些老问题重新变得显眼。
Agent 程序里,代码、计划、工具调用和中间决策都可能由模型参与生成。于是系统面对的不再只是普通 bug,还有格式错误、参数错误、语义错误、幻觉、不可重放副作用和资源冲突。
这些问题并不是完全无根的,很多都能从历史里找答案:
- 输出不稳定:回到类型系统和 schema;
- 长程 Agent 对话上下文:回到状态和 Effect;
- workflow 需要并行和恢复:回到 dataflow、KPN 和 durable execution;
- 资源冲突和长期状态:回到 Actor、BOC 或其他并发模型。
所以 AI 时代不是突然出现一批完全陌生的问题,而是把很多程序系统里的经典问题重新组合在了一起。
回看 PL 和程序系统历史,能帮助我们判断今天的 Agent 框架到底在解决什么问题,哪些能力只是包装,哪些能力真的触及了运行时语义。
面向过程和面向对象只是入口
回到最开始的问题:编程语言除了面向过程和面向对象,现在发展到哪了?
我的理解是,面向过程和面向对象没有错,它们只是编程语言地图上的一角。继续往后看,会发现程序结构、合法性、组合、副作用、并发、失败、变化、正确性,都可以成为语言、框架和 runtime 的一部分。
这些概念不是为了炫技,也不是抽象越多越好。它们出现,是因为系统复杂到一定程度以后,某些责任不应该继续只靠程序员脑子硬扛。
学无止境,不是说要追更多新名词,而是要不断扩展自己理解程序的方式。
程序可以是指令流,可以是对象网络,可以是函数组合,可以是数据流图,可以是 actor 之间的消息,可以是 effect 描述,可以是可恢复 workflow,也可以是带证明的结构。
学到后面会发现,很多新的语言、框架和 runtime,其实都在反复做同一件事:把复杂性从人的经验里拿出来,变成系统可以理解和处理的结构。
参考资料
这些资料是后续扩写时可以回看的入口。部分是概念源头,部分是工程实现或现代框架示例。
语言谱系与编译器
- Computer Languages History (accessed: 2026-05-09)
- Seven Languages in Seven Weeks (accessed: 2026-05-09)
- Seven More Languages in Seven Weeks (accessed: 2026-05-09)
- Stanford CS143: Compilers (accessed: 2026-05-09)
- The Cool Reference Manual (accessed: 2026-05-09)
- LLVM Language Reference Manual (accessed: 2026-05-09)
Effect 与函数式编程
- ZIO Core Reference (accessed: 2026-05-09)
- Cats Effect Docs (accessed: 2026-05-09)
并发、数据流与调度模型
- Pekko Typed Actors (accessed: 2026-05-09)
- Erlang/OTP (accessed: 2026-05-09)
- Communicating Sequential Processes (accessed: 2026-05-09)
- Ptolemy II Process Networks Domain (accessed: 2026-05-09)
- BOC: When Concurrency Matters (accessed: 2026-05-09)
- Apache Flink Fault Tolerance (accessed: 2026-05-09)
失败恢复与持久执行
- Temporal Docs (accessed: 2026-05-09)
变化、增量与复用
- React Reconciliation (accessed: 2026-05-09)
- Salsa: A Generic Framework for On-Demand, Incrementalized Computation (accessed: 2026-05-09)
- Frank McSherry's Differential Dataflow Blog (accessed: 2026-05-09)
- Materialize Docs (accessed: 2026-05-09)
正确性与证明方向
- Rocq Prover, formerly Coq (accessed: 2026-05-09)
- Rocq Documentation (accessed: 2026-05-09)
AI / Agent 交叉
- Anthropic: Building Effective Agents (accessed: 2026-05-09)
- OpenAI Agents SDK Docs (accessed: 2026-05-09)
- LangGraph Persistence (accessed: 2026-05-09)
- PAgE 2026: Principles of Agentic Engineering (accessed: 2026-05-09)