跳转到内容

Literate Programming — 把程序写成给人读的文章

待复核

日常类比:普通代码像厨房后场的备料清单,只有厨师看得懂;literate programming 像一本有步骤、有旁白、有索引的菜谱,读者先知道为什么这样切,再看到真正下锅的动作。

Knuth 在 1984 年这篇文章里说的不是“多写注释”,而是换一个主语:写程序时,第一目标不是指挥机器,而是向人解释“我想让机器做什么”。

他把这个想法做成 WEB 系统:一份 .web 源文件同时生成两样东西。WEAVE 生成排版漂亮、带目录和索引的说明书;TANGLE 生成编译器能吃的 Pascal 程序。

所以 literate programming 的核心是:代码和解释同源,阅读顺序服务人,执行顺序交给工具重排

不理解 Knuth 这篇文章,下面这些事都不好解释:

  • 为什么“文档过期”是软件工程里最常见的失败模式——因为文档和代码通常分开维护。
  • 为什么源码顺序不总适合教学——编译器要先声明变量,人类往往想先看目标和动机。
  • 为什么 Knuth 会把程序叫作“literature”——他关心的是命名、结构、叙事和读者理解。
  • 为什么现代 notebook、docs-as-code、README-driven development 都有它的影子——它们都在争取让解释和可执行物更靠近。
  1. 同一个源头,两个出口.web 文件像一张总菜单,WEAVE 做给人读的说明书,TANGLE 做给机器跑的源码。类比:一份剧本既能排成观众节目单,也能拆成后台走位表。

  2. 顺序按理解来,不按编译器来:WEB 允许先讲“打印前 1000 个素数”的整体计划,再逐段补变量、循环、格式化细节。类比:讲故事先说主线,再补人物关系,而不是按身份证号码介绍角色。

  3. 解释会反过来改善代码:Knuth 发现自己进入“讲课模式”后,调试时间下降,因为含糊的设计很难被清楚讲出来。类比:你一旦要教别人做一道题,就会先把自己脑内偷懒的步骤补全。

案例 1:一个最小的“织”和“缠”结构

Section titled “案例 1:一个最小的“织”和“缠”结构”
@* 打印问候语。
这一节先告诉读者:程序只做一件事,向屏幕输出一句话。
@p
program hello(output);
begin
@<输出问候语@>;
end.
@<输出问候语@>=
write('hello, reader');

逐部分解释

  • @* 开一个给人看的大节,适合写动机和背景。
  • @p 标出真正的 Pascal 程序入口,TANGLE 会从这里开始拼源码。
  • @<输出问候语@> 是命名代码块,人先读名字,机器之后再展开。
  • WEAVE 保留解释并排版,TANGLE 丢掉解释只产出可编译代码。

案例 2:把“阅读顺序”和“运行顺序”拆开

Section titled “案例 2:把“阅读顺序”和“运行顺序”拆开”
@<主流程@>=
@<读配置@>;
@<处理数据@>;
@<写结果@>
@<处理数据@>=
if input_is_empty then
@<报告空输入并停止@>
else
@<正常转换@>

逐部分解释

  • 读者先看 主流程,立刻知道程序分三步。
  • 复杂分支被放到后面单独解释,主流程不会被异常处理淹没。
  • 编译器最后看到的仍是一段普通控制流,工具负责把块展开到合法位置。
  • 这就是 Knuth 说的“web”:不是一棵严格自顶向下的树,而是一组互相连接的小块。

案例 3:用 change file 保持主版本干净

Section titled “案例 3:用 change file 保持主版本干净”
@x
@d new_page == page
@y
@d new_page == write_ln('--- page break ---')
@z

逐部分解释

  • @x@y 表示“在主 WEB 文件里找到这段旧文本”。
  • @y@z 表示“在本机器上换成这段新文本”。
  • 不同机器的差异写在 .ch change file 里,不直接改主 .web
  • 这让 TeXware 可以跨 IBM、Xerox、HP 等环境移植,同时保留同一份主逻辑。
  1. 把它理解成“注释写多一点”:原因是普通注释仍跟着编译器顺序走,WEB 的重点是让解释顺序独立于机器顺序。

  2. 把文档和代码复制两份:原因是两份材料一分家就会漂移,literate programming 要求同一份源头生成两种出口。

  3. 忽略工具复杂度:原因是 WEB 同时混合 TeX、Pascal 和自己的语法,新人可能分不清错误来自排版、语言还是算法。

  4. 把“漂亮排版”当成全部价值:原因是排版只是 WEAVE 的表层收益,真正的收益是设计时被迫讲清动机和不变量。

适用

  • 教学型代码:算法、编译器、数据库内核、密码学实现,读者需要理解“为什么这么写”。
  • 长寿命系统:未来维护者比当前机器更重要,解释要和源码一起演化。
  • 研究型软件:程序本身就是论文证据,读者需要从动机一路追到实现。
  • 需要移植的系统:主逻辑稳定,平台差异用 change file 或类似机制隔离。

不适用

  • 快速试错脚本:一小时后就删的脚本不值得投入完整叙事结构。
  • 主要由 GUI/配置拼出来的系统:代码不是主要知识载体时,WEB 式源码组织收益有限。
  • 团队没有共同工具链:如果没人会生成、阅读、评审 woven 文档,源文件会变成额外负担。
  • 高频重构的早期产品:结构还没稳定时,过早雕琢文章会拖慢探索。
  • 1970s:结构化编程让程序更可靠,但 Knuth 觉得“能证明”还不等于“好理解”。
  • 1978 年:Tony Hoare 建议 Knuth 公开 TeX 程序,这逼他思考怎样把大型真实软件写到别人能读。
  • 1979 年:Knuth 开始设计 WEB 的前身 DOC,用 TeX 做排版、Pascal 做实现语言。
  • 1981-1982 年:WEB 逐步稳定,Knuth 用它写 TANGLE、WEAVE 和 TeX 相关程序。
  • 1983 年:Stanford 技术报告《The WEB System of Structured Documentation》给出完整系统说明。
  • 1984 年:这篇《Literate Programming》发表,把工具经验上升成一套工程哲学。
  • 之后:CWEB、noweb、Jupyter notebook、R Markdown 等系统都继承了“解释和代码相互靠近”的想法。
  1. 程序首先是给人维护的知识制品——机器只需要最终源码,人需要动机、顺序、名字和索引。

  2. 文档不是事后补丁,而是设计动作本身——一段逻辑讲不清,往往说明它还没有设计好。

  3. 工具可以把人的顺序翻译成机器顺序——这和编译器把高级语言翻译成机器码是同一种工程信念。

  4. “适合阅读”也是一种性能指标——Knuth 声称写 WEB 没让总时间变长,因为更好的解释换来了更少调试。

  • 论文 PDF:Knuth 1984 Literate Programming(本文原文,PRIMES.WEB 示例最值得看)。
  • 系统报告:Knuth 1983 “The WEB System of Structured Documentation”,paper-context 从参考文献中抽到的直接前置材料。
  • 结构化编程:Dahl, Dijkstra, Hoare 1972 Structured Programming(理解 Knuth 为什么要接着谈“下一步”)。
  • 平衡论:dijkstra-goto —— Knuth 1974 反对把结构化编程变成教条。
  • knuth-taocp —— TeX 和 WEB 都从 Knuth 对“程序作为作品”的长期执念里长出来。
  • program-comprehension-fmri —— 后来的认知研究从另一个角度说明“读代码”真的是软件工程核心活动。
  • knuth-taocp —— TAOCP 训练的是算法表达,WEB 训练的是程序叙事。
  • dijkstra-goto —— 结构化编程先解决控制流可推理,literate programming 再解决整体可理解。
  • algol-60 —— ALGOL 传统影响 Pascal,WEB 的 TANGLE 最终就产出 Pascal。
  • hoare-logic —— Hoare 关心程序如何证明,Knuth 关心程序如何被读懂,两者都反对随手糊代码。
  • cognitive-load-theory —— WEB 把读者一次要记的东西拆小,正是在降低工作记忆压力。
  • program-comprehension-fmri —— 读程序不是顺手动作,而是需要专门支持的认知任务。
  • texstudio —— TeX 生态的现代编辑器,能看到 Knuth 对排版工具链的长期影响。

(暂无反向链接)