Literate Programming — 把程序写成给人读的文章
待复核日常类比:普通代码像厨房后场的备料清单,只有厨师看得懂;literate programming 像一本有步骤、有旁白、有索引的菜谱,读者先知道为什么这样切,再看到真正下锅的动作。
Knuth 在 1984 年这篇文章里说的不是“多写注释”,而是换一个主语:写程序时,第一目标不是指挥机器,而是向人解释“我想让机器做什么”。
他把这个想法做成 WEB 系统:一份 .web 源文件同时生成两样东西。WEAVE 生成排版漂亮、带目录和索引的说明书;TANGLE 生成编译器能吃的 Pascal 程序。
所以 literate programming 的核心是:代码和解释同源,阅读顺序服务人,执行顺序交给工具重排。
不理解 Knuth 这篇文章,下面这些事都不好解释:
- 为什么“文档过期”是软件工程里最常见的失败模式——因为文档和代码通常分开维护。
- 为什么源码顺序不总适合教学——编译器要先声明变量,人类往往想先看目标和动机。
- 为什么 Knuth 会把程序叫作“literature”——他关心的是命名、结构、叙事和读者理解。
- 为什么现代 notebook、docs-as-code、README-driven development 都有它的影子——它们都在争取让解释和可执行物更靠近。
-
同一个源头,两个出口:
.web文件像一张总菜单,WEAVE做给人读的说明书,TANGLE做给机器跑的源码。类比:一份剧本既能排成观众节目单,也能拆成后台走位表。 -
顺序按理解来,不按编译器来:WEB 允许先讲“打印前 1000 个素数”的整体计划,再逐段补变量、循环、格式化细节。类比:讲故事先说主线,再补人物关系,而不是按身份证号码介绍角色。
-
解释会反过来改善代码:Knuth 发现自己进入“讲课模式”后,调试时间下降,因为含糊的设计很难被清楚讲出来。类比:你一旦要教别人做一道题,就会先把自己脑内偷懒的步骤补全。
案例 1:一个最小的“织”和“缠”结构
Section titled “案例 1:一个最小的“织”和“缠”结构”@* 打印问候语。这一节先告诉读者:程序只做一件事,向屏幕输出一句话。
@pprogram 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表示“在本机器上换成这段新文本”。- 不同机器的差异写在
.chchange file 里,不直接改主.web。 - 这让 TeXware 可以跨 IBM、Xerox、HP 等环境移植,同时保留同一份主逻辑。
-
把它理解成“注释写多一点”:原因是普通注释仍跟着编译器顺序走,WEB 的重点是让解释顺序独立于机器顺序。
-
把文档和代码复制两份:原因是两份材料一分家就会漂移,literate programming 要求同一份源头生成两种出口。
-
忽略工具复杂度:原因是 WEB 同时混合 TeX、Pascal 和自己的语法,新人可能分不清错误来自排版、语言还是算法。
-
把“漂亮排版”当成全部价值:原因是排版只是 WEAVE 的表层收益,真正的收益是设计时被迫讲清动机和不变量。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 教学型代码:算法、编译器、数据库内核、密码学实现,读者需要理解“为什么这么写”。
- 长寿命系统:未来维护者比当前机器更重要,解释要和源码一起演化。
- 研究型软件:程序本身就是论文证据,读者需要从动机一路追到实现。
- 需要移植的系统:主逻辑稳定,平台差异用 change file 或类似机制隔离。
不适用:
- 快速试错脚本:一小时后就删的脚本不值得投入完整叙事结构。
- 主要由 GUI/配置拼出来的系统:代码不是主要知识载体时,WEB 式源码组织收益有限。
- 团队没有共同工具链:如果没人会生成、阅读、评审 woven 文档,源文件会变成额外负担。
- 高频重构的早期产品:结构还没稳定时,过早雕琢文章会拖慢探索。
历史小故事(可跳过)
Section titled “历史小故事(可跳过)”- 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 等系统都继承了“解释和代码相互靠近”的想法。
-
程序首先是给人维护的知识制品——机器只需要最终源码,人需要动机、顺序、名字和索引。
-
文档不是事后补丁,而是设计动作本身——一段逻辑讲不清,往往说明它还没有设计好。
-
工具可以把人的顺序翻译成机器顺序——这和编译器把高级语言翻译成机器码是同一种工程信念。
-
“适合阅读”也是一种性能指标——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 对排版工具链的长期影响。
(暂无反向链接)