为什么并非所有事物都可扩展?

为什么并非所有事物都可扩展?

TeX 的宏处理器通过一个称为扩张对于输入的 token 流,宏处理器重复扩展直到剩下不可扩展的标记为止。生成的不可扩展标记流被传递给 TeX 的执行处理器。扩展过程可以看作是调用一个函数,该函数将扩展为它的结果。

宏从输入流中吸收参数,并扩展为替换文本,参数就位。其他类型的标记可以以不同的方式扩展:例如,条件测试其参数(也可能扩展它们)并跳过条件为假的分支。

但也有一些不太平凡的代币不是可扩展:最值得注意的\def是(实际上不是令牌,但见下文)寄存器分配。这意味着它们不能在宏中使用以通过扩展获得结果:它们将原封不动地传递。

例如,

\edef\test{\def\a{x}\a}

将失败! Undefined control sequence.,因为\def没有扩展,然后\a被检查,结果证明是未定义的。

同样地,

\newcount\count
\edef\test{\count=1}
\showthe\count

将显示0,而不是1,因为再次,没有一个\count=1是可扩展的。

我们可以想象一个这样的操作的系统可扩展。更准确地说,扩展\def将从输入流中吸收控制序列名称、参数文本和替换文本,定义新宏并扩展为空。类似地,名为的操作\assign将从输入中读取寄存器名称和值,执行赋值并扩展为空。这也可以扩展到等\let\advance

因此,上述示例现在会表现不同:首先\edef\def会读入\a{x},定义\a并扩展为空文本。经过这次扩展后,标记列表将包含\a,然后扩展为x

在第二个例子中(假设

\edef\test{\assign\count1}

现在)\assign将设置\count1,并扩展为无。结果中的\test将被定义为空,但的值\count将被改变。

这个新系统将允许以更直接的方式实现某些事情。例如,定义一个宏扩展为n星号现在可以用

\newcount\c
\def\asterisks#1{%
  \assign\c0
  \loop\ifnum\c<#1
    *%
    \advance\c by 1
  \repeat}

\loop,因为(参见和的定义\iterate\def\let和赋值现在可扩展。另一个实质性的结果是,可以在宏中完成更多的事情,其结果作为参数传递给另一个宏。观察 e-TeX\numexpr及其朋友如何已经朝着这个方向迈出了重要的一步。

问题是:为什么 TeX 不实现这种方法,而是让一些重要的操作不可扩展?这种方法的缺点是什么?TeX 实现的优点是什么?

一个可能的原因是 Knuth 希望宏能够充当纯函数,无法改变它们正在扩展的上下文。在电子书就此事:

可扩展标记的扩展发生在 TeX 的“嘴巴”中,但原始命令(包括赋值)则在 TeX 的“胃”中完成。这种结构的一个重要结果是,在 TeX 扩展标记列表(例如或命令)时,不可能重新定义控制序列或推进寄存器\message\write赋值操作仅在 TeX 构建垂直或水平或数学列表时进行。

另一个原因可能是,如果嵌套和/或递归宏调用对可用的“外部”数据具有写访问权限,则它们可能会相互干扰。

注意:问题不在于 TeX 的架构允许什么和不允许什么,而是为什么首先设计这样的架构。

答案1

虽然只有参与 TeX 开发的斯坦福团队,尤其是 Knuth 教授才能给出明确的答案,但我认为我们可以看到一些可能的原因。

首先,Knuth 设计 TeX 主要是为了解决一个特定的问题(排版,计算机编程艺术)。他让 TeX 足够强大,能够解决他面临的排版问题,以及他决定解决的更一般的情况。然而,他也让 TeX (几乎) 尽可能简单必要的来实现这一点。虽然可扩展宏很有用,但它们并不是解决许多问题所必需的。

其次,在某些情况下,可扩展方法至少可能会产生歧义。Bruno 的\edef\foo{\def\foo{abc}}例子就很好。我想说,在这里,可扩展方法的预期结果\def\foo扩展到零,但我也想说这并不完全清楚。更常见的情况是,你想要类似

\begingroup
\edef\x{%
 \endgroup
 \def\noexpand\foo{\csname some-macro-to-fully-expand\endcsname}%
 }
 \x

使用可扩展原语会使其更加复杂。

上面的例子指出了另一个灰色区域:对于诸如\begingroup和 之类的事情会发生什么,更重要的是\relax。 后者是不可扩展的无操作这一事实在 TeX 编程中通常很重要。(事实上,\numexprETC。,吞噬可选的尾随\relax有时被认为是一件坏事。)

最后,我认为易于实现也很重要。将扩展和执行步骤分开的方法使流程相对容易理解,而且我认为也容易实现。混合扩展和执行的方法需要更复杂的架构。在这里,我们必须记住 Knuth 编写 TeX 的时候,我们今天认为理所当然的编程思想在 20 世纪 70 年代后期并不一定适用。我认为完全可扩展的方法会使代码更复杂、更慢。当 TeX 在“大型”计算机上运行时,速度影响非常重要。

答案2

每次看到这个问题,我就觉得应该还有点什么要说,所以现在让我来谈谈。

从我的角度来看,TeX 的设计意图相对扩展的目的是它应该提供一种手段来转化简单输入进入复杂的排版指令。它只是一种编程语言,因为它可以对虚拟印刷机进行编程,但不是机器的内部工作原理。只有在人们能够理解的情况下,它才是图灵完备的(如果一个人疯了/布鲁诺·勒弗洛克:) )重新实现那台印刷机之内TeX 只使用可扩展宏(正如所展示的(参见lambda 包) 可以实现 lambda 演算)。

根据这一点,TeX 的正确思维模型是一台看起来很疯狂的计算机印刷机,它读取一长串穿孔卡片(记得 Knuth 第一次看到计算机是在什么时代,以及他在什么时代编写了 TeX),这些穿孔卡片填充了它的寄存器,访问了它的字体,并构建了它的水平、垂直和数学列表。这些穿孔卡片来自较早机器读取你的文件并将其编码为 TeX 基元,偶尔会从印刷机接收一些要编码的文本(例如 中的文本\hbox)。卡片生成机,在 TeXbook 更有机的比喻中被称为“嘴巴”,它只负责将 .tex 文件渲染为印刷机的合适输入;它(可能更类似于卡片)排序计算机(或称为机器)只是接收一些关于如何处理控制序列的指令,并相应地安排输入。

编辑:简短的版本是“嘴巴 = 语法”和“胃 = 语义”,其中“嘴巴 = 可扩展”和“胃 = 可执行”。1982errorlog.pdf年 9 月 18 日的有趣历史评论中明确说明了这一点:“\expandafter通过将其从语义转移到语法 [即从胃转移到嘴巴],使其更加强大。”)

由于印刷机可以在中途改变指令,并且可扩展的控制序列是视觉上与不可扩展的原语相同。但我相信,它得到了“嘴”和“胃”的类似隐喻以及你从 TeXbook 中引用的内容的支持:“基于语言”的扩展编程和“基于机器”的执行编程实际上是两种完全不同且不是TeX 中的对称设施。后者的需求决定了前者的进程,而前者完全处于从属地位。

答案3

Knuth 并不想创造一种全新的编程语言,他只想拥有一种排版语言。Guy Steele 曾游说将 TeX 打造为图灵完备语言 [纹理“图灵完备”这个词不知为何被广泛使用。

Guy Steele 因合作撰写了 Lambda 论文而闻名 [拉姆达] 导致了编程语言 Scheme 的出现。顺便说一句,该语言曾用于计算机科学教科书《计算机程序的结构和解释》[标准化过程控制计划] 已被世界各地大学用作计算机科学课程的基础,并且将继续被使用。[采纳]。

在麻省理工学院最初的计算机科学课程中,作者 Harold Abelson 和 Gerald Jay Sussman 一路介绍了许多编程范式。直到课程中间,他们才引入了程序状态 [状态] 值得注意的是他们如何强调在语言中引入状态(/分配/时间/副作用)的复杂性。

CS 仍然是一个年轻的事物,从上述视频的制作到现在已经有 30 年了。从那时到现在,人们已经对所谓的“纯函数式语言”进行了大量研究。其中一种特别活跃的语言叫做 Haskell [哈斯克尔]。

我之所以提到 Haskell,是因为在它的创始人之一 Simon Peyton Jones 的一次演讲中,我意识到任何类型的 I/O 都需要状态。因此,即使是 MIT CS 视频中 Scheme 的 REPL(Read-Eval-Print-Loop)的使用,严格来说也不是纯函数式编程,因为没有状态就无法获得任何类型的输出。换句话说,纯函数式表达式,严格意义上来说,只是某些机器中的一些执行,知道它在做什么的唯一方法是机器变得更热(引用 Jones 先生的话 [斯派克] 这里)。

同样,TeX 或任何其他语言输出的任何东西都不可能“纯粹”,因为只要你想要输出,你就需要状态。诀窍是控制这些副作用。

我喜欢这样看待 TeX,即使这可能并不完全准确,那就是将所谓的“嘴”视为功能部分,将“胃”视为命令式的、“具有状态”的部分,即分配发生的地方。我认为 Ryan 或 Andrew 曾经说过,有时感觉 TeX 就像一个巨大的拼图。我必须同意;这就像一定有一种方法,通过将我们所知道的关于 CS 的一切放在一起,以可控的方式控制 TeX 的副作用,以实现更易于编程的环境。

相关内容