5.5 命令式编程 VS 声明式编程
从所有问题都手写逐步控制流走到把策略选择交给有全局信息的执行器,沿命令式步骤、声明式目标与计划成本验证结果。
学习目标
- 能沿描述目标、解析约束、生成计划、执行计划和核对结果比较命令式与声明式程序
- 能解释命令式控制流、声明式结果描述、执行器计划和资源成本之间的边界
- 能在正常、边界和故障场景中回答:为什么声明式描述隐藏了步骤,却没有消除执行计划与成本
为什么需要这一机制
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 5.5 命令式编程 VS 声明式编程。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
5.5 命令式编程 VS 声明式编程 不能停在“一个写步骤,一个写结果”。真实系统要解决的变化是从 所有问题都手写逐步控制流 到 把策略选择交给有全局信息的执行器;命令式程序明确给出状态变化步骤,声明式程序描述期望关系或结果,执行器再根据数据、约束和资源选择计划。两者可以在不同层次组合,关键是把计划和代价重新纳入证据链。
三个会让编程范式比较失真的陷阱
一个目录节点到计划证据
5.5 命令式编程 VS 声明式编程
在 ↡沿目标、约束、计划、执行和结果验证比较命令式步骤与声明式描述,并把执行成本纳入判断的方法。 中,执行器选择计划可以先表示为
这不是要求每个系统都实现数学优化,而是提醒我们:声明式目标仍然需要可行性约束、计划选择、资源成本和结果核对。命令式程序把一部分计划提前写进控制流,声明式程序把一部分计划交给执行器,二者都要接受同一份验收合同。
描述目标
↡只声明期望结果、关系和边界,不直接规定每个状态变化步骤的输入阶段。先把结果合同写清楚,包括输入、输出、排序、权限、一致性和失败语义。目标含糊时,执行器无法生成可验证计划。
一则小故事
↡用一个任务中手写步骤与交给执行器选择路径的对照故事,引出控制权和全局信息差异的节点。故事只能帮助记忆问题。真正的比较要回到同一数据、约束、资源和结果,不能让比喻替代计划证据。
命令式编程
↡由程序员显式规定状态变化、控制流、操作顺序和错误处理步骤的编程方式。命令式代码容易看到局部顺序,但程序员也要承担全局计划、并发、缓存和恢复策略。步骤可见不等于成本已经可接受。
声明式编程
↡描述期望结果与约束,把部分求解策略和执行顺序交给拥有全局信息的执行器的编程方式。声明式代码减少了手写控制流,但执行器仍要解析约束、选择计划、消耗资源并返回可核对结果。隐藏步骤不等于消除步骤。
解析约束
↡读取数据规模、索引、权限、资源、并发和失败边界,并把它们变成计划选择依据的阶段。约束决定哪些计划可行。只看目标不看约束,容易在边界规模或权限变化时选错路径。
生成计划
↡根据目标与约束选择算子、访问路径、顺序和资源分配,并形成可观察执行方案的阶段。计划是声明式执行器的关键证据,也是命令式程序需要显式审查的部分。应保存计划版本、估算和实际成本。
执行计划
↡按选定算子和顺序读取数据、改变状态并记录时序、资源和中途错误的阶段。执行期间关注扫描量、CPU、内存、锁、I/O、尾延迟和回滚。最终输出相同不能抹去中途的资源超限。
核对结果
↡将实际输出、状态、成本和失败语义与目标合同及基线比较,并判断是否可接受的阶段。核对不只是比较文本,还要比较顺序、完整性、资源预算、重试和恢复后的状态。
最小可重放实现
goal = describe_result(input, constraints)
plan = choose_plan(goal, data_stats, resources)
actual = execute(plan)
evidence = record(plan, actual, cost, failures)
assert verify(goal, actual, evidence)这段实现草图只表达命令式与声明式计划的验证合同,不复制原书叙事或代码。实际运行时应保存输入、目标、约束、计划、算子、资源、执行时序、输出、失败和复位结果,使另一位读者能够从干净状态重放。
五步复核一次编程范式选择
1. 固定目标与输入边界
记录输入、输出、排序、权限、一致性、资源预算和失败语义。先预测命令式步骤与声明式目标各自会暴露哪些证据,再运行基线。
Lab
命令式与声明式计划实验
先预测偏差会落在步骤、计划还是资源成本,再切换显式控制、执行器选计划与隐藏成本样本。
程序明确指定遍历、过滤和写入的顺序
imperative=steps → state=explicit → order=fixed → output=observed
判定
accept:状态变化与控制流可追踪
当前样本:命令式步骤;保存目标、约束、计划、资源、执行时序、输出和复位轨迹。
正常、边界与故障证据矩阵
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 目标、数据、约束、版本和资源固定 | 两种实现都满足结果合同 | 目标、计划、输出、资源、重放 |
| 边界 | 数据规模、索引或资源预算改变 | 计划或步骤变化可解释 | 约束、估算、实际成本、尾延迟 |
| 故障 | 计划误选、资源耗尽或执行器异常 | 拒绝只凭最终结果判等价 | 最早偏离、错误、回滚、恢复 |
故障诊断:先分开目标、计划与资源
- 目标侧:查输入、输出、排序、权限、一致性和失败语义;合同不清时不要先争论写法。
- 约束侧:查数据规模、统计信息、索引、资源、并发和版本;声明式执行器需要这些全局信息。
- 计划侧:查实际算子、访问路径、顺序、估算与实际行数;结果相同仍可能选了错误计划。
- 执行侧:查 CPU、内存、锁、I/O、尾延迟、中途错误和回滚;命令式局部步骤也必须接受全局成本检查。
如果两种实现输出不同,先比目标合同与排序;如果输出相同但延迟恶化,查计划和数据规模;如果声明式结果超预算,查统计信息、索引与执行器版本。每次只改变一个控制量,并重放基线。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 5.5 命令式编程 VS 声明式编程
用同一目标、约束、计划和资源证据比较两种编程方式的方法。
- 一则小故事
用任务中的步骤与执行器选路径对照引出控制权差异的记忆节点。
- 命令式编程
显式规定状态变化、控制流、操作顺序和错误处理步骤的方式。
- 声明式编程
描述结果与约束并把部分求解策略交给执行器的方式。
- 生成计划
根据目标和约束选择算子、路径、顺序与资源分配的阶段。
- 核对结果
将输出、状态、成本和失败语义与目标合同和基线比较的阶段。
练习
练习
问题 1(5.5 命令式编程 VS 声明式编程、 一则小故事): 为什么“描述结果”不等于声明式程序没有执行步骤?
问题 2(命令式编程、声明式编程): 比较命令式循环和声明式查询时,怎样避免只看最终输出?
问题 3(描述目标、解析约束、生成计划、执行计划、核对结果): 声明式版本在小数据上通过,大数据上尾延迟超预算,应该沿哪条证据链诊断?
本页小结
5.5 命令式编程 VS 声明式编程 的关键不是给两种写法排优劣,而是把目标、约束、计划、执行和结果放进同一条证据链。完成标准是解释隐藏控制流的边界,定位计划成本的最早偏离,并证明重置后同一输入可以重放。