1.1 文艺编程(Literate Programming)
请注意
本项目使用 Markdown 整理翻译,所以格式未遵循原文的文艺编程规范,还请谅解。
在创建 \( \text{T}_\text{E}\text{X} \) 排版系统时,唐纳德·克努斯(Donald Knuth)开发了一种基于简单但革命性思想的新编程方法论。引用克努斯的话:“让我们改变对程序构建的传统态度:与其想象我们的主要任务是指示计算机该做什么,不如集中精力向人类解释我们希望计算机做什么。”他将这种方法论称为 文艺编程(literate programming)。本书(包括您现在阅读的章节)是一个长篇文艺程序(literate program)。这意味着在阅读本书的过程中,您将阅读 pbrt 渲染系统的完整实现,而不仅仅是对它的高层次描述。
文艺程序是用一种混合文档格式语言(例如 \( \text{T}_\text{E}\text{X} \) 或 HTML)和编程语言(例如 C++)的元语言编写的。两个独立的系统处理该程序:“编织(weaver)”将可读程序转换为适合排版的文档,“解开(tangler)”生成适合编译的源代码。我们的可读编程系统是自制的,但受到诺曼·拉姆齐(Norman Ramsey)的 noweb 系统的重大影响。
文艺编程元语言提供了两个重要特性。第一个是将散文与源代码混合的能力。这个特性使得程序的描述与其实际源代码平起平坐,鼓励仔细的设计和文档编写。第二,该语言提供了以与编译器输入完全不同的顺序向读者呈现程序代码的机制。因此,程序可以以逻辑的方式进行描述。每个命名的代码块称为 片段(fragment),每个片段可以通过名称引用其他片段。
作为一个简单的例子,考虑一个函数 InitGlobals() ,它负责初始化程序的所有全局变量:†(本节的代码仅用于说明,并不是 pbrt 中的部分)
void InitGlobals() {
nMarbles = 25.7;
shoeSize = 13;
dielectric = true;
}
尽管这个函数很简短,但没有任何上下文很难理解。例如,为什么变量 nMarbles 可以取浮点值?仅仅查看代码,人们需要搜索整个程序,以查看每个变量的声明位置以及它是如何使用的,以理解其目的和合法值的含义。尽管这种系统结构对于编译器来说很好,但人类读者更希望看到每个变量的初始化代码单独呈现,靠近声明和使用该变量的代码。
在一个文艺程序中,可以这样写 InitGlobals() :
/** 函数定义 */
void InitGlobals() {
/** 初始化全局变量 */
shoeSize = 13;
dielectric = true;
}
这定义了一个片段,称为 <<函数定义>>,其中包含 InitGlobals() 函数的定义。 InitGlobals() 函数本身引用了另一个片段 <<初始化全局变量>>。由于初始化片段尚未定义,我们对这个函数一无所知,只知道它可能包含对全局变量的赋值。(不过,我们可以通过点击它右侧的加号提前查看;这样做会展开该片段的所有最终代码。)
仅仅拥有片段名称在目前是合适的抽象层次,因为尚未声明任何变量。当我们在程序的后面某处引入全局变量 shoeSize 时,我们可以写。
/** 初始化全局变量 */
shoeSize = 13;
在这里,我们开始定义 <<初始化全局变量>> 的内容。当文艺程序被编译成源代码时,文艺编程系统会在 InitGlobals() 函数的定义中替换代码 shoeSize = 13; 。等号后面的 \( \blacktriangledown \) 符号表示稍后将向该片段添加更多代码。点击它可以带您到发生该操作的地方。
在文本后面,我们可以定义另一个全局变量 dielectric ,并可以将其初始化附加到该片段中:
/** 初始化全局变量 */
dielectric = true;
在片段名称后面的 += 符号表示我们已添加到先前定义的片段。此外,\( \blacktriangle \)符号链接回之前 <<初始化全局变量>> 添加代码的地方。
当解开时,这三个片段变成代码
void InitGlobals() {
// 初始化全局变量
shoeSize = 13;
dielectric = true;
}
通过这种方式,我们可以将复杂的函数分解为逻辑上不同的部分,使它们更容易理解。例如,我们可以将一个复杂的函数写成一系列片段:
/** 函数定义 */
void complexFunc(int x, int y, double *values) {
<<Check validity of arguments>>
/** 检查参数的有效性 */
if (x < y) {
/** 交换 x 和 y */
}
/** 循环前进行预计算 */
/** 循环遍历并更新 values 数组 */
}
再次,每个片段的内容在 complexFunc() 中展开以供编译。在文档中,我们可以依次介绍每个片段及其实现。这种分解使我们能够逐行呈现代码,从而更容易理解。这种编程风格的另一个优点是,通过将功能分成逻辑片段,每个片段都有一个单一且明确的目的,每个片段都可以独立编写、验证或阅读。一般来说,我们会尽量使每个片段少于 10 行。
在某种意义上,文艺编程系统只是一个增强的宏替换包,专门用于重新排列程序源代码。这看起来可能是一个微不足道的变化,但实际上,文艺编程与其他软件系统结构化方式有很大不同。