使用 Noweb 的文学编程

作者:Andrew L. Johnson

本质上,文学编程 (LP) 的目的可以在以下引言中找到

“让我们改变我们对程序构建的传统态度:与其想象我们的主要任务是指导计算机做什么,不如专注于向人类解释我们希望计算机做什么。”—Donald E. Knuth,1984 年。

这种环境颠覆了在代码中以注释形式包含文档的概念,转变为将代码嵌入到程序描述中。这样做,文学编程促进了计算机程序的开发和呈现,这些程序更紧密地遵循从问题空间到解决方案空间的概念地图。反过来,这使得程序更容易调试和维护。

在编写文学程序时,人们在单个源文件中指定程序描述和程序代码,其顺序最适合人类理解。程序代码可以从该文件中提取并组装成编译器或解释器可以理解的形式——这个过程称为“缠结 (tangling)”。文档通过“编织 (weaving)”描述和代码成准备排版的形式(最常见的是 TeX 或 LATeX)来生成。

多年来,人们创建了许多不同的文学编程工具。大多数更流行的工具都是直接或概念上基于 D. E. Knuth 创建的 WEB 系统(“文学编程”,计算机杂志 (27)2:97-111, 1984)。本文重点介绍 Norman Ramsey 的 noweb——一个易于使用、可扩展且独立于目标编程语言的文学编程工具。

Noweb 系统概述

当您使用 noweb 编写文学程序时,您会创建一个简单的文本文件(按照惯例,其扩展名为 .nw),您可以在其中为程序的各个部分提供所有技术文档,以及程序每个部分的实际源代码。

Literate Programming Using Noweb

图 1. Perl 脚本的排版版本

这个文件( 列表 1 ),我们称之为 nw 源文件,然后由 noweave 处理,以创建准备排版的文档(程序的排版版本如图 1 所示),或者由 notangle 处理,以提取代码块并将它们按正确的顺序组装起来,供编译器或解释器使用(程序的可执行版本在 列表 2 )。这两个过程不是独立的程序,而是一组过滤器,nw 源文件通过这些过滤器进行管道传输。正是这种管道系统使 noweb 既灵活又可扩展,因为可以修改管道,并且可以创建新的过滤器并插入到管道中以更改 noweb 的行为。

像大多数文学编程工具一样,noweb 依赖于 TeX 或 LATeX——(LA)TeX 指的是两者——用于排版文档(尽管它也具有生成 HTML 输出的选项)。但是,人们不需要成为(LA)TeX 大师也能产生良好的结果。交叉引用、索引和排版代码的所有繁重工作都由 noweave 自动处理。

排版文档

了解 noweb 功能的最佳方法是参考成品:程序的排版版本。图 1 代表了一个 Perl 脚本的排版版本,该脚本实际上通过提供有限的“autodefs”过滤器来扩展了 noweb 的功能。此过滤器将识别并标记包和子例程名称以进行自动交叉引用和索引。

在查看此示例时,人们可以很快看到实际代码块是如何散布在描述性文本中的。每个代码块都通过页码和一个字母子页引用唯一标识。例如,在图 1 中,第一页上有四个代码块,在左边距中标记为 1a、1b、1c 和 1d。

除了边距标签外,每个代码块的第一行还在左边距处带有其名称和块引用,在右边距处可能带有交叉引用信息。让我们更仔细地检查块 1b——其第一行的合理副本是

1b <

这一行告诉我们,我们现在在块 1b 中。<全局变量 1a>+= 构造告诉我们,我们正在处理名为 全局变量 的块,其定义从块 1a 开始。+= 表示我们正在添加到 全局变量 的定义中。在右边距,我们遇到 (1d) <1a 1c>,这意味着我们正在定义的块在块 1d 中使用,并且当前块从块 1a 继续,并将进一步在块 1c 中继续。应该注意的是,所有这些视觉交叉引用线索——除了块名称本身——都由 noweb 自动提供。

在任何块的末尾都有两个可选的脚注——“定义”脚注和“使用”脚注。用户可以在 nw 源文件中手动指定当前块中定义的标识符(即,变量或子例程)列表。此外,如果使用编程语言的“autodefs”过滤器,则可以自动识别某些标识符。(有适用于多种语言的 autodefs 过滤器,包括 C、Icon、TeX、yacc 和 Pascal)。

这些标识符列在定义它们的块下方的“定义”脚注中,以及对任何使用它们的块的引用。任何已定义标识符的出现都会在“使用”脚注中引用,该脚注位于使用该标识符的块下方。

例如,在图 1 中,我们看到块 1c 定义了术语 $index_prefix,该术语在块 2b 中使用。快速查看块 2b 证实了,确实,这个术语被使用了,并出现在该块的“使用”脚注中。

块 1d,autodefs.perl,代表了我们整个程序的顶层描述。此块在 noweb 中被称为“根”块,并且不在任何其他块中使用。我们的示例只有一个根块,尽管您可以在 nw 源文件中定义任意多个,并且 notangle 可以将它们中的每一个提取到单独的文件中。

块 1d 中代码的第一行是强制性的 #!/usr/bin/perl 行,它必须作为所有旨在作为可执行程序调用的 Perl 脚本的开头。但是,接下来的两行根本不是 Perl 代码行,而是对其他命名块定义的引用。来自这些被引用块的代码将插入到 notangle 提取的可执行程序中的此点。因此,我们对程序有一个广泛的概述,没有被特定的全局变量初始化和子例程定义所干扰。

查看块 2a,它包含在我们的根块中,我们看到它也包含另一个块,块 2b。这表明块的包含可以嵌套到几乎任何级别,并且可以以文档中的任何顺序发生(定义不需要先于使用)。

我们的文档以 noweb 提供的两个可选索引结束——代码块索引和标识符索引。

在 Noweb 中编写程序

在了解了管道末端输出的内容之后,我们现在可以描述 nw 源文件本身的结构。我们的示例程序的 nw 源文件在列表 1 中给出。

当您编写 noweb 程序时,您需要在解释一段代码和提供该段代码的正式定义之间交替。您必须通过使用两个 noweb 标签之一来指示您是输入文档还是代码。

要开始编写文档,请在左列中使用 @ 符号开头,后跟空格或换行符。这表示至少到下一个标签为止的所有后续文本都是文档文本。所有文档文本都通过过滤过程传递到 (LA)TeX 文件。因此,作者负责提供文档中可能需要或需要的任何特殊格式,例如节、表、脚注和数学公式。

除了标准的 (LA)TeX 命令集之外,noweb 还提供了三个额外的控制序列。文本中由双中括号括起来的任何文本都以与文字代码相同的方式排版,并且 \nowebindex 和 \nowebchunks 命令扩展为如图 1 所示的示例末尾显示的两种索引。

要指示代码块的开始,请使用双角括号括起代码块的名称,后跟等号

<<code_chunk_name>>=

此构造之后的所有内容都被视为文字代码或对另一个块名称的引用。您可以通过将另一个块名称放在没有尾随等号的双角括号中来引用它。与文档一样,当遇到另一个标签时,代码块终止。要继续代码块定义,请使用与要继续的块相同的名称在括号内启动新的代码块。

代码块的特殊格式和交叉引用由 noweb 自动处理,不需要用户进行特殊输入——手动指定标识符定义除外。

要手动列出给定块中定义的标识符,请使用以下形式的行终止该块

@ %def

该行上给出的标识符将放置在该块的“定义”脚注中,并将由 noweb 自动交叉引用和索引,如上一节所述。

notangle 将代码提取到适合编译器或解释器的形式的过程仅遵循几个简单的规则。根块在命令行上指定为要提取和组装的块。然后逐行打印此块,直到遇到对另一个块的引用。此时,逐行输出被引用的块——对于其中引用的任何块也是如此。当被引用的块输出后,notangle 继续输出根块的过程。

当处理继续的块——两个或多个共享相同名称的块——时,notangle 将它们的定义按出现顺序连接成一个单独的命名块。我们的示例程序的提取代码在列表 2 中,可以看出,所有空格和缩进都在可执行版本中得到适当保留。

由于 notangle 提取和组装其输入的方式,程序可以以最适合人类理解的顺序呈现和解释。notangle 将确保程序块以适合编译器或解释器的顺序排列。

咒语

现在我们知道如何在 noweb 中创建程序,我们可以检查生成程序的排版版本和可执行版本的方法。noweb 发行版提供了一个通用的 shell 脚本,名为 noweb,它驱动 notangle 和 noweave 进程。但是,这种调用方法虽然简单,但在某种程度上受到限制。我们将在此处重点介绍单独使用每个工具,因为这种方法提供了更灵活的方法。

当您调用 notangle 时,您需要指定要从 nw 源文件中提取和组装的块名称(根块)。如果您未能指定块,notangle 将搜索名为 * 的块以提取(这是 noweb 程序中的默认根块)。notangle 工具写入 stdout,因此您必须将其重定向到您选择的文件。命令的一般形式是

notangle [-R
        [-filter

因此,要提取我们的示例程序的可执行版本,我们使用

notangle -Rautodefs.perl autodefs.perl.nw >
         autodefs.perl
-R 选项指定要提取的根块。-L 选项用于嵌入行指令,如果编译器/解释器支持它们。行指令引用 nw 源文件中的位置。因此,在调试代码时,您永远不需要参考可执行版本。相反,您可以在 nw 源文件中编辑代码。默认格式用于 C 预处理器,但它也适用于 Perl,但有一个问题。每当输入或返回块时,都会发出行指令,并引用下一行代码。因此,在像我们这样的脚本中,行指令最终会作为可执行版本的第一行出现在 #! 行之前,使其不可执行。对此的修复方法是删除第一行指令,或将其移动到第一行下方并将行号加一。

人们可以编写过滤器以与 notangle 或 noweave 一起使用,这些过滤器在源进入管道后对其进行操作。noweb 中 nw 源文件的管道表示超出了本文的范围(请参阅发行版文档中包含的“Noweb Hacker's Guide”)。但是,应该注意的是,可以轻松构建一个过滤器来自动解决行指令问题。

程序的排版版本是使用 noweave 工具生成的。noweave 有几个有用的选项,所有选项都在手册页中详细说明。我们在这里只考虑一些最重要的选项。

第一个普遍感兴趣的选项涉及所需的输出:您可以指定 -latex(默认)、-tex-html 作为用于最终文档的格式化语言。这些选项中的每一个都将为排版版本提供适当的包装器(可以使用 -n 选项抑制)。您可以编写旨在用于 LATeX 排版的 nw 源文件,并且仍然可以选择通过使用 -html 选项和发行版中包含的 LATeX 到 html 过滤器 (-filter l2h) 调用 noweave 来生成 HTML 文档。

-x 选项启用块名称的交叉引用和索引,以及任何由“autodefs”过滤器自动识别的标识符。使用 -index 选项意味着 -x,并为手动定义的标识符(在 nw 源文件中的 @ %def 语句中提到的标识符)提供交叉引用和索引。

通常,noweave 会插入附加信息,例如用于页眉的文件名,作为其包装器的一部分。-delay 选项使 noweave 暂停插入此信息,直到第一个文档块之后。当您希望提供自己的 (LA)TeX 包装器以指定其他包或定义您自己的特殊格式化命令时,这最有用。这意味着 -n(省略包装器)选项,并且要求您确保在文件末尾的文档块中包含 \end{document} 控制序列以完成包装器。我们的示例 nw 源文件就是以这种方式编写的。

我们的排版版本(图 1)是通过首先使用 notangle 提取 autodefs.perl 根块并使用 chmod 系统命令使其可执行来生成的。然后,我们将此可执行文件放在 noweb 库目录中,并将 noweave 调用为

noweave -autodefs perl -delay -index
        autodefs.perl.nw > autodefs.tex

然后,我们可以在生成的文件上运行 LATeX 两次(以解决页面引用),以创建 dvi 文件,然后使用 dvips 创建 postscript 版本以包含在本文中。

其他选项允许您从外部文件创建索引,展开制表符,并指定包含的 noweb.sty 文件提供的替代格式化选项。后者包括省略左边距中的块编号、更改代码块中的文本大小以及从发生在右边距的代码块的符号交叉引用切换到类似于“定义”和“使用”脚注风格的简单脚注样式交叉引用。

结论

总的来说,文学程序在最初制作时需要更多的时间和精力。但是,由于最初的许多努力都致力于解释程序的每个部分,因此作者最终可能会生成质量更高的程序,因为她在游戏的每个阶段都对程序的设计进行了更多的思考。此外,通过投入额外的精力来创建文档完善的程序,以后在维护和升级程序上花费的时间会大大减少。

就文档和解释而言,按照组件在程序设计中发挥作用的顺序(而不是它们必须按照编译器或解释器的顺序出现的顺序)来描述组件的能力,是对传统注释代码的巨大改进。除了改进代码和更容易维护的好处之外,文学程序还可以很好地用作教学工具。

可用性和注释

Andrew Johnson 目前是一名全日制学生,正在攻读物理人类学博士学位,同时也是一名兼职程序员和技术作家。他和妻子及两个儿子住在马尼托巴省温尼伯,只要有机会,他就喜欢喝一杯黑啤酒。可以通过 ajohnson@gpu.srv.ualberta.ca 与他联系。

Brad Johnson 目前正在马尼托巴大学攻读统计学学位。

加载 Disqus 评论