1970年6月,E.F. Codd在《ACM通讯》上发表了一篇注定改变计算机科学进程的论文。在这篇题为《大型共享数据库的关系模型》的文章中,他提出了一个看似简单的想法:能否让用户只描述想要什么数据,而不必关心如何找到它们?

这个问题的答案,催生了SQL语言和整个关系数据库产业。但很少有人意识到,Codd的设计决策开启了一个持续至今的编程范式博弈——声明式编程的七十年演进史,是一部关于人类如何与机器协商控制权的史诗。

当"怎么做"变成"要什么"

理解声明式编程,需要从它的反面开始。

命令式编程的核心隐喻是菜谱。打开任何一本编程入门教材,你会看到类似的代码:创建一个变量,循环遍历数组,判断条件,执行操作。每一步都明确指定了"怎么做"——就像菜谱告诉你"先切洋葱,再热油,然后炒三分钟"。

声明式编程则更像是在餐厅点菜。你对服务员说"我要一份牛排,五分熟",至于厨房如何选肉、如何控制火候、如何摆盘,都不是你需要操心的事。SQL就是这种思维的典型代表:

SELECT name, salary FROM employees WHERE department = 'Engineering' ORDER BY salary DESC;

这段代码只描述了想要什么:工程部门所有员工的名字和薪水,按薪水降序排列。至于数据库如何扫描表、是否使用索引、如何排序,全部由查询优化器决定。

这种抽象带来的好处是显而易见的。同一个SQL查询,在十年前的数据库上和在今天的数据库上,可能采用完全不同的执行策略——而开发者无需修改任何代码。数据库引擎的每次升级,都能自动惠及所有现有查询。

但这里隐藏着一个关键假设:系统比人类更懂得如何优化。

flowchart LR
    subgraph 命令式["命令式编程"]
        A1["步骤1: 创建变量"] --> A2["步骤2: 循环遍历"]
        A2 --> A3["步骤3: 条件判断"]
        A3 --> A4["步骤4: 执行操作"]
    end
    
    subgraph 声明式["声明式编程"]
        B1["描述目标状态"] --> B2["系统自动执行"]
    end
    
    命令式 -- 控制权在开发者 --> 声明式
    声明式 -- 控制权在系统 --> 命令式

SQL的豪赌:信任优化器

当Don Chamberlin和Ray Boyce在1974年设计SEQUEL(SQL的前身)时,他们做了一个大胆的决定。当时的数据库界主流是"导航式"系统——用户必须明确指定如何在记录之间跳转,沿着预先定义的路径从一个记录导航到另一个。Charles Bachman因此获得了1973年的图灵奖,他在获奖演讲中将程序员比作"在数据空间中航行的导航员"。

Chamberlin和Boyce的目标用户不是程序员,而是"城市规划师或保险分析师"——这些人的工作需要访问大量数据,但不想成为计算机专家。他们希望设计一种"走上去就能读"(walk-up-and-read)的语言,让没有任何编程经验的人也能理解查询的含义。

为了实现这个目标,他们必须解决一个核心问题:如果用户不告诉数据库如何查找数据,谁来决定?

答案就是查询优化器。它的工作是将声明式的SQL查询翻译成命令式的执行计划。一个看似简单的查询,可能有数十种甚至数百种执行方式。优化器需要在毫秒级别内评估各种方案的成本,选择最优的一个。

flowchart TB
    subgraph 输入["声明式输入"]
        Q["SQL查询<br/>SELECT ... FROM ... WHERE ..."]
    end
    
    subgraph 优化器["查询优化器"]
        P1["解析查询"] --> P2["生成候选计划"]
        P2 --> P3["成本估算"]
        P3 --> P4["选择最优计划"]
    end
    
    subgraph 输出["命令式输出"]
        E["执行计划<br/>索引扫描 → 哈希连接 → 排序"]
    end
    
    输入 --> 优化器
    优化器 --> 输出

考虑这样一个查询:

SELECT users.id, (SELECT SUM(posts.likes) FROM posts WHERE posts.user_id = users.id) FROM users;

字面上看,这需要两层嵌套循环:外层遍历用户表,内层遍历帖子表,时间复杂度是O(|users| × |posts|)。但如果优化器足够聪明,它可以将其转换为一次哈希连接和聚合操作,时间复杂度降为O(|users| + |posts|)。对于百万级的数据,这意味着从几小时缩短到几秒。

SQL的成功证明了Chamberlin和Boyce的豪赌是对的——至少在大多数情况下。查询优化器确实能做出比人类更好的决策,而且随着时间推移越来越强。今天,PostgreSQL的优化器可以处理极其复杂的查询,Oracle甚至引入了机器学习来预测执行成本。

但声明式的承诺在这里已经出现了第一道裂痕:当优化器选错计划时,你几乎没有任何控制手段。

想象一个场景:某个关键查询突然变慢了。你检查执行计划,发现优化器选择了一个糟糕的索引策略。你可以做什么?在纯粹的声明式世界中,答案是什么都做不了——你只能接受优化器的判断。当然,现实中的数据库提供了各种"提示"(hint)机制,允许你干预优化器的决策。但这是对声明式原则的妥协,承认了"有时候系统不知道什么最好"。

React的困境:当状态形状与UI树不匹配

如果说SQL的声明式模型面临的挑战主要来自性能优化,那么前端框架中的声明式UI则遇到了更深层的问题:状态与视图的形状不匹配

2013年,React团队提出了一个革命性的想法:与其手动操作DOM,不如让开发者描述UI应该长什么样,然后由框架自动计算出最小化的DOM操作。这个想法的核心是虚拟DOM和协调算法。

虚拟DOM本质上是一个JavaScript对象树,代表了真实DOM应该呈现的样子。每当状态变化,React会创建一个新的虚拟DOM树,然后与旧树比较,计算出需要更新的真实DOM节点。这个过程被称为"协调"(reconciliation)。

这套机制在简单场景下运作良好。如果你的应用状态结构与组件树结构一致——例如,一个待办事项列表组件渲染一个待办事项数组——React的声明式模型既简洁又高效。

但考虑一个更复杂的场景:一个画布编辑器应用。用户选中的对象需要同时在三个地方呈现:中间的画布上(可视化展示)、左侧边栏的对象层级树中、右侧边栏的属性面板里。这三个UI区域在组件树中相距甚远,但它们观察的是同一块状态。

graph TB
    subgraph 组件树["React组件树"]
        Root["App"] --> Canvas["Canvas<br/>(画布区域)"]
        Root --> Left["LeftSidebar<br/>(层级树)"]
        Root --> Right["RightSidebar<br/>(属性面板)"]
    end
    
    subgraph 状态["共享状态"]
        Selected["selectedObject"]
    end
    
    Selected -.->|需要渲染| Canvas
    Selected -.->|需要渲染| Left
    Selected -.->|需要渲染| Right
    
    style Selected fill:#f9f,stroke:#333,stroke-width:2px

按照React的设计哲学,你应该将共享状态"提升"到最近的共同祖先组件,然后通过props逐层传递下去。但这会导致一个尴尬的局面:每当用户选中一个对象,从顶层组件向下的一大片子树都需要重新渲染。如果你的组件中包含useEffect或useMemo,每次状态变化都会触发一连串的重新计算和闭包重建。

问题的根源在于React的一个基本假设:状态的依赖图形状应该与组件树形状相似。当这个假设成立时,React的声明式API确实能提供极佳的开发体验。但当它不成立时——这在复杂应用中相当常见——开发者就会陷入与框架的拉锯战,不断使用useMemo、useCallback、React.memo等"逃逸舱"来阻止不必要的渲染。

这正是Signals等细粒度响应式方案兴起的原因。Signals允许你定义一个独立于组件树的依赖图,精确追踪哪些状态变化会影响哪些UI片段。代价是放弃了纯粹的声明式模型——你需要更明确地思考状态如何流动,而不是简单地"描述UI应该长什么样"。

flowchart LR
    subgraph React模型["React声明式模型"]
        R1["状态变化"] --> R2["重新渲染子树"]
        R2 --> R3["虚拟DOM比对"]
        R3 --> R4["更新真实DOM"]
    end
    
    subgraph Signals模型["Signals细粒度响应"]
        S1["状态变化"] --> S2["追踪依赖图"]
        S2 --> S3["仅更新相关节点"]
    end
    
    React模型 -- 粗粒度更新 --> Signals模型
    Signals模型 -- 细粒度控制 --> React模型

一位资深开发者的抱怨道出了这种张力:“当你花在优化性能上的时间超过了构建功能的时间,声明式的承诺就开始显得空洞。你不再是描述目标状态,而是在研究框架内部的执行机制,试图通过各种提示来避免过度渲染。”

Kubernetes:永不休止的调和循环

将视线从用户界面转向基础设施,我们会发现声明式范式面临的另一重挑战:永恒变化的现实世界

Kubernetes的核心设计理念是"期望状态"(desired state)与"实际状态"(actual state)的分离。你声明"我想要三个Nginx副本",Kubernetes的控制循环会持续工作,确保实际运行的副本数始终接近三。这个过程被称为"调和"(reconciliation)。

这种设计的优雅之处在于它天然处理了失败场景。如果某个Pod崩溃了,控制器会自动创建新的来替代。如果节点故障了,调度器会将Pod重新分配到其他节点。开发者不需要编写复杂的错误处理逻辑——只需要声明期望状态,剩下的交给系统。

flowchart LR
    subgraph 声明["开发者声明"]
        D["期望状态: 3个副本"]
    end
    
    subgraph 控制器["Kubernetes控制器"]
        C1["监控实际状态"] --> C2["比较差异"]
        C2 --> C3{"是否一致?"}
        C3 -- 否 --> C4["执行调和动作"]
        C4 --> C1
        C3 -- 是 --> C1
    end
    
    subgraph 集群["集群现实"]
        R["实际状态: 运行中的Pod"]
    end
    
    声明 --> 控制器
    控制器 --> 集群
    集群 --> 控制器

但声明式的承诺在这里也遇到了现实的重击。首先是状态漂移(state drift)问题:实际状态可能因为各种原因偏离期望状态——人为的手动干预、外部系统的变更、硬件故障。虽然控制器会尝试纠正这些偏差,但诊断偏差的原因往往需要深入理解系统的执行细节。

更深层的问题是收敛时间的不可预测性。在纯粹的声明式模型中,你只关心最终状态,不关心中间过程。但在生产环境中,“最终"可能意味着几秒钟,也可能意味着几分钟。一个Deployment的滚动更新可能因为镜像拉取慢、节点资源不足、网络问题等原因而停滞在某个中间状态。这时候,你需要深入命令式世界——检查事件日志、手动删除卡住的Pod、调整资源限制。

Kubernetes社区也逐渐意识到了这个问题。Custom Resource Definitions和Operator模式的兴起,本质上是在声明式的框架内嵌入更多的命令式逻辑。一个Operator不仅是控制循环,还包含了特定领域的业务逻辑——比如数据库的备份恢复、集群的扩缩容。这种设计让系统变得更强大,但也更复杂,离"只声明期望状态"的理想越来越远。

七十年的范式博弈

声明式编程的历史可以追溯到比SQL更早的年代。

timeline
    title 声明式编程七十年演进
    section 早期奠基
        1958 : Lisp诞生<br/>McCarthy创造函数式编程
        1970 : Codd发表关系模型论文
        1972 : Prolog诞生<br/>逻辑编程范式
        1974 : SEQUEL/SQL设计完成
        1977 : Make构建工具发布
    section 现代复兴
        2013 : React发布<br/>声明式UI革命
        2014 : Kubernetes项目启动
        2019 : SwiftUI发布<br/>苹果拥抱声明式
        2020 : Flutter成熟<br/>跨平台声明式UI

1958年,John McCarthy在MIT发明了Lisp。虽然Lisp通常被归类为函数式语言,但它的一些核心特性——将计算描述为数学函数的组合,而非状态的逐步转换——已经蕴含了声明式编程的精神。McCarthy的初衷是为人工智能研究创建一种符号处理工具,但Lisp的影响远远超出了这个领域。

1972年,Alain Colmerauer和Robert Kowalski在马赛大学开发了Prolog。这是一种更加纯粹的声明式语言:程序不是指令序列,而是一组逻辑规则。你定义事实和规则,然后提出问题,Prolog的推理引擎会自动寻找答案。Prolog的理论基础是一阶谓词演算,这为声明式编程提供了坚实的数学根基。

1977年,Stuart Feldman在贝尔实验室创建了Make。这是一个构建自动化工具,程序员只需描述文件之间的依赖关系,Make会自动确定需要重新编译哪些文件。Make的Makefile是一种声明式的依赖图描述,它不需要你编写复杂的构建脚本——只需要告诉系统"这个文件依赖那些文件”。

这些早期系统的共同特点是:它们都试图将某个特定领域的问题从命令式的细节中解放出来。SQL处理数据查询,Prolog处理逻辑推理,Make处理构建依赖。每个系统都找到了自己的"甜蜜点"——一个问题域,在这个域中,声明式抽象的收益超过了它的代价。

优雅的代价

声明式编程的优雅是显而易见的。它降低了认知负担,因为你只需要关心"要什么";它提高了可维护性,因为实现细节被隐藏在抽象层之下;它支持自动优化,因为系统拥有更多的自由度来选择执行策略。

但这种优雅是有代价的。

第一个代价是调试困难。当声明式系统不按预期工作时,诊断问题往往需要深入理解系统的内部实现。一个React组件为什么重新渲染了?一个SQL查询为什么变慢了?一个Kubernetes Pod为什么一直Pending?回答这些问题需要超越声明式API,理解底层的执行机制。

第二个代价是性能控制权的丧失。声明式系统承诺自动优化,但当优化效果不理想时,你往往没有太多干预手段。SQL的查询提示、React的memoization、Kubernetes的资源限制——这些都是对纯粹声明式原则的妥协,承认了有时候系统需要人类的指导。

第三个代价是学习曲线。虽然声明式API在表面上更简单,但真正掌握它需要理解底层的执行模型。一个能写出SQL查询的开发者,不一定能理解为什么某个查询会触发全表扫描;一个能写React组件的开发者,不一定能解释为什么useEffect的依赖数组会触发意外的重新执行。

最根本的代价是:声明式系统基于一系列假设,当这些假设失效时,整个模型就会崩溃。SQL假设优化器比人类更懂性能优化,React假设状态结构与组件树结构相似,Kubernetes假设期望状态总能达成。现实世界并不总是配合这些假设。

graph TD
    subgraph 声明式承诺["声明式承诺"]
        A["描述目标即可"]
        B["自动优化"]
        C["简化开发"]
    end
    
    subgraph 现实代价["现实代价"]
        D["调试黑盒"]
        E["性能失控"]
        F["学习曲线陡峭"]
    end
    
    A -->|"失效时"| D
    B -->|"优化失败"| E
    C -->|"深入理解后"| F
    
    style A fill:#d4f1d4
    style B fill:#d4f1d4
    style C fill:#d4f1d4
    style D fill:#ffd4d4
    style E fill:#ffd4d4
    style F fill:#ffd4d4

边缘地带的挣扎

有趣的是,当声明式系统面对边缘情况时,它们往往会引入一些"命令式逃逸舱"。

SQL有存储过程和触发器,允许你在声明式框架内嵌入命令式逻辑。React有useEffect和useLayoutEffect,让你手动控制副作用。Kubernetes有init containers和lifecycle hooks,提供更细粒度的控制。

这些机制的存在说明了一个深刻的事实:纯粹的声明式模型无法覆盖所有场景。当一个声明式系统足够成熟时,它必然会发展出命令式的扩展机制——不是因为设计者缺乏原则,而是因为现实世界的复杂性需要更灵活的工具。

但这带来了新的问题。当你混合使用声明式和命令式风格时,系统的行为变得更难预测。一个带有复杂useEffect的React组件,既不是纯粹的声明式,也不是清晰的命令式,而是某种混乱的混合体。一个使用大量触发器的数据库,其行为可能比纯粹的过程式代码更难理解。

下一个七十年

声明式编程的未来在哪里?

一个可能的方向是更智能的执行引擎。现代数据库正在引入机器学习来改进查询优化,React团队也在探索编译时优化来减少运行时开销。如果系统能更准确地理解开发者的意图,声明式模型的有效范围就能扩大。

另一个方向是更好的"中间地带"。不是纯粹的声明式,也不是完全的命令式,而是某种可控的半声明式系统。Signals和细粒度响应式可能代表了这个方向——你仍然声明期望状态,但也能更精确地控制更新的粒度。

还有人尝试从根本上重新思考声明式编程的形式化基础。代数效应(algebraic effects)、差分数据流(differential dataflow)等技术试图提供更严谨的理论框架,让声明式系统在保持抽象的同时也能处理更复杂的场景。

flowchart TB
    subgraph 过去["过去七十年"]
        P1["Lisp/Prolog<br/>理论基础"]
        P2["SQL/Make<br/>领域特定"]
        P3["React/K8s<br/>广泛应用"]
    end
    
    subgraph 未来["未来方向"]
        F1["智能优化<br/>ML驱动"]
        F2["细粒度响应<br/>Signals范式"]
        F3["形式化基础<br/>代数效应"]
    end
    
    过去 --> 未来

但无论技术如何演进,声明式编程的核心张力不会消失:人类想要控制权,但也想要简化的抽象;机器想要自由度,但也需要明确的约束。这不仅是技术问题,也是关于人类如何与复杂系统共处的哲学问题。

或许,最好的答案不是选择声明式或命令式,而是理解它们各自适合的场景。SQL适合数据查询,但可能不适合复杂的业务逻辑;React适合状态与视图对齐的UI,但可能不适合需要精细性能控制的场景;Kubernetes适合管理无状态服务,但可能不适合有复杂状态依赖的系统。

七十年的历史教会我们:没有银弹,只有权衡。声明式编程的优雅是真实的,它的局限也是真实的。理解这种张力,才能在合适的地方使用合适的工具,而不是被任何一种范式绑架。


参考资料

  1. Codd, E.F. (1970). “A Relational Model of Data for Large Shared Data Banks”. Communications of the ACM, 13(6), 377-387.

  2. Chamberlin, D.D. & Boyce, R.F. (1974). “SEQUEL: A Structured English Query Language”. Proceedings of the 1974 ACM SIGFIDET Workshop on Data Description, Access and Control, 249-264.

  3. Chamberlin, D.D. (2020). “50 Years of Queries”. Communications of the ACM, 63(10), 46-55.

  4. Colmerauer, A. & Roussel, P. (1996). “The Birth of Prolog”. History of Programming Languages II, 331-367.

  5. McCarthy, J. (1960). “Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I”. Communications of the ACM, 3(4), 184-195.

  6. Feldman, S.I. (1979). “Make—A Program for Maintaining Computer Programs”. Software: Practice and Experience, 9(4), 255-265.

  7. React Documentation. “Reconciliation”. https://legacy.reactjs.org/docs/reconciliation.html

  8. Kubernetes Documentation. “Controllers”. https://kubernetes.io/docs/concepts/architecture/controller/

  9. “When Declarative Systems Break”. Interjected Future, 2025. https://interjectedfuture.com/when-declarative-systems-break/

  10. “Unexplanations: Query Optimization Works Because SQL is Declarative”. Scattered Thoughts, 2024. https://www.scattered-thoughts.net/writing/unexplanations-sql-declarative/

  11. “SwiftUI vs UIKit: Why is Declarative Programming the Future?”. Espeo Software, 2023.

  12. “The Evolution of Programming Paradigms”. LearnYard. https://read.learnyard.com/low-level-design/the-evolution-of-programming-paradigms/

  13. “Functional Programming vs Declarative Programming vs Imperative Programming”. Stack Overflow Discussion, 2012.

  14. “Is Functional Programming Declarative?”. Eric Normand Podcast, 2018.

  15. Wikipedia. “Declarative Programming”. https://en.wikipedia.org/wiki/Declarative_programming

  16. Wikipedia. “Make (Software)”. https://en.wikipedia.org/wiki/Make_(software)

  17. “The Elm Architecture with React”. DEV Community, 2025.

  18. “Kubernetes and Reconciliation Patterns”. Hossein Kassaei, 2023.

  19. “Drift Detection in IaC: Prevent Your Infrastructure from Breaking”. env0, 2025.

  20. “CSS is a Declarative, Domain-Specific Programming Language”. Laura Kalbag, 2018.