\chapter[programming]{编程} 在第 \in[chinese-fonts] 章的开始,简单论述了 \TEX\ 与 \CONTEXT\ 以及 \LATEX\ 的关系。\TEX\ 系统的精妙之处在于,它是可编程的,是开放的,它像是给我们提供了一些积木以及一些简单的组合规则,使得我们也能具备建造像 \LATEX\ 和 \CONTEXT\ 这些上层建筑的能力。我并不是说,你也应当再搭建某种上层建筑。实际上即使在日常使用 \LATEX\ 或 \CONTEXT\ 时,\TEX\ 的编程机制依然非常有用,甚至你可以从这些工作里逐步获得建造上层建筑的能力。 我在之前的一些章节里也曾粗略提及了 \TEX\ 的编程机制,诸如自定义一些简单的宏,也对 \CONTEXT\ LMTX 所支持的 Lua 编程机制作了一些介绍。本章尝试对这两种编程机制再作一些专门的探讨。不过,我并没有足够的精力和动力成为 \TEX\ 编程专家,我所能做的仅仅是为你在使用 \CONTEXT\ 时开启一个更为神秘且宏大的视角。 \section[tex-macro]{宏} 也许你用的输入法不太方便打出直角引号。在 \TEX\ 里,只需要定义一个简单的宏便可让此事无需劳烦输入法。例如 \startexample \def\zhqt#1{「#1」} \zhqt{被中文直角引号包含的文本} \stopexample \simpleexample[option=TEX]{\getexample} 对于 \TEX\ 普通用户而言,宏最大的用处是提供简写。是的,你无需编程,便已经能得到宏编程机制的好处了,甚至可以将宏的名字定义为中文: \startexample \def\引号#1{「#1」} \引号{被中文直角引号包含的文本} \stopexample \simpleexample[option=TEX]{\getexample} \noindent 像 \type{\zhqt} 这样的宏称为有参数的宏。在向宏传递参数时,花括号实际上并非必须,例如 \startexample \def\zhqt#1{「#1」} \zhqt被中文直角引号包含的文本 \stopexample \simpleexample[option=TEX]{\getexample} \noindent 输出的结果里,只有「被」字会被直角引号包含。 对于有参数的宏而言,用花括号构造的编组,会被 \TEX\ 视为一个整体,与单个字符等效。下面的例子定义了带有 2 个参数的宏,它不仅能给文字加上中文直角引号,而且还能将文字渲染为指定的颜色。 \startexample \def\zhqt#1#2{「\color[#1]{#2}」} \zhqt{red}{被中文直角引号包含的文本} \stopexample \simpleexample[option=TEX]{\getexample} 在定义有参数宏时,若参数两侧存在某些符号时,则在使用宏时也必须提供相同的符号,这种符号称为宏参数的定界符。例如 \startTEX \def\zhqt[#1]#2{「\color[#1]{#2}」} \zhqt[red]{被中文直角引号包含的文本} \stopTEX 至此,也许你觉得这一切似乎平平无奇。我们定义一个有参数的宏,在使用它时,不过是向其喂入一些文本,再等待它吐出我们期待的文本——该过程称为「宏的展开」——,然而当你尝试让宏「吐出」自身时,就会遇到一件奇怪的事情。例如 \startTEX \def\foo{\foo} \foo \stopTEX \noindent 当 \TEX\ 在试图展开 \tex{foo} 时,它便会陷入无休止的状态,因为它会对展开所得 \tex{foo} 再度展开,亦即 \TEX\ 总是努力将一个宏展开至无所展开时为止。这件奇怪的事情实际上已经触及到编程的本质——递归。我建议你不要轻易尝试上述示例,除非你知道如何关闭一个正在努力工作的程序。 \section{变量} 若想将 \TEX\ 从上一节中 \tex{foo} 无尽展开的地狱里解救出来,必须通过寄存器,保存某种状态,从而有机会确定何时截止 \TEX\ 对 \tex{foo} 的展开。 早期的 \TEX\ 提供了 256 个可用于保存整数的寄存器。\CONTEXT\ 所用的 luameta\TEX\ 已将这类寄存器的数量拓展为数以万计。可以用 \tex{newcount} 命令获得一个尚未被使用的整数寄存器,例如 \startexample \newcount\mynum \mynum = 42 {\bf 宇宙的秘密是 \the\mynum。} \stopexample \simpleexample[option=TEX]{\getexample} 可以对整数寄存器中的数据做数学运算,例如加法或减法: \startexample \newcount\mynum \mynum = 42 \advance\mynum by 1 % \the\mynum + 1 \advance\mynum by -1 % \the\mynum - 1 {\bf 宇宙的秘密是 \the\mynum。} \stopexample \simpleexample[option=TEX]{\getexample} 若你有幸学过汇编语言,此刻应该不难感受到一些熟悉的气息。除了整数寄存器,\TEX\ 还有其他类型的寄存器,最为常用的有尺寸寄存器与盒子寄存器。例如,在 \CONTEXT\ 经常使用的一个命令 \tex{textwidth},它实际上是尺寸寄存器,其中存储的值是正文版面的宽度。至于盒子寄存器,在 \in[box-depth] 节便已对其有所领略。 现在可不必关心 \TEX\ 究竟有哪些寄存器以及它们如何使用。不过,若你已经熟悉某种或某些编程语言,我建议你将 \TEX\ 的寄存器视为特定类型的变量。例如整数寄存器,你可以理解为是整型变量。尺寸寄存器,可以理解为实数变量。盒子寄存器,可以理解为结构体或对象。使用 \tex{def} 定义的一些无参数的宏,可理解为字符串变量——上一节定义的 \tex{foo} 宏是个例外。 \section{条件} \TEX\ 提供了 \tex{ifnum} 命令,可用于比较两个整数的大小或是否相等,例如 \startexample \newcount\mynum \mynum = 42 \ifnum\mynum = 42 宇宙的秘密是 \the\mynum。\else\relax\fi \stopexample \simpleexample[option=TEX]{\getexample} \noindent 上述代码里所做的比较是,如果 \tex{mynum} 寄存器里存储的值为 42,则输出宇宙的秘密,否则什么都不做——可将 \tex{relax} 命令理解为它什么都不做……或者无为。若将 \type{=} 更换为 \type{<} 或 \type{>} 则分别可比较 \tex{mynum} 中的值小于或大于 42。也许将上述示例中换为更为正经的编程语言来表达,能让你看得更清楚一些,例如用 C 语言: \starttyping /BTEX\color[darkgreen]{int}/ETEX mynum = 42; /BTEX\color[darkblue]{if}/ETEX (mynum == 42) { printf(/BTEX\color[darkred]{"宇宙的秘密是 \%d。"}/ETEX, mynum); } /BTEX\color[darkblue]{else}/ETEX ; \stoptyping \TEX\ 也提供了其他形式的条件命令,诸如 \tex{ifdim}、\tex{ifodd}、\tex{ifhmode} 等,这些条件命令还是在需要它们出现的时候再探讨其用法吧。 \section{函数} 现在,我们有能力拯救从 \tex{foo} 里解救 \TEX\ 了,例如 \startexample \def\foo#1{% \ifnum #1 < 42 \advance #1 by 1 \foo{#1}% \else\relax% \fi% } \newcount\TEST \TEST = 0 \foo\TEST 宇宙的秘密是 \the\TEST。 \stopexample \simpleexample[option=TEX]{\getexample} \noindent 注意,宏定义里每一行末尾的注释符通常是有必要的,它的作用是防止在宏的展开结果里引入额外的换行符。此刻的 \tex{foo} 像是一个可控的递归函数。 通过上述示例,想必你已领悟,在使用有参数的宏时,若其参数是一个命令,也可以不用编组,因为 \TEX\ 会将一个命令视为一个整体传给宏。为了让你更为深刻理解宏的本质抑或它的凶险,我对上例 \tex{ifnum} 语句略作改动,如下 \startTEX \ifnum #1 < 42 \advance #1 by 1\foo{#1}% \stopTEX \noindent 你可以先思考一下结果会当如何,然后验证一下。 倘若你愿意将有参数的宏视为函数或计算过程,也许能感受到 \TEX\ 颇为有趣的地方——宏既可以表达数据,也可以表达代码或运算。在正经的编程语言里,大概只有 Lisp 系的语言拥有类似神韵。 \section{Lua} 在 Lua 语言里——可能你不懂这门编程语言,但它很简单——,也可以定义一个函数,使之类似于 \in[tex-macro] 节中 \tex{foo} 宏,如下 \startLUA function foo() foo() end \stopLUA \CONTEXT\ 允许我们在 \type{luacode} 环境里编写 Lua 代码,例如 \startLUA \startluacode function foo() foo() end \stopluacode \stopLUA \CONTEXT\ 也允许我们通过宏去调用 Lua 函数,若调用上述的 \type{foo} 函数,只需 \startTEX \ctxlua{foo()} \stopTEX \noindent 倘若你动手操练了上述代码,应该能看到 \CONTEXT\ 会给出类似以下编译错误: \starttyping token call, execute: [ctxlua]:3: stack overflow stack traceback: ... ... ... \stoptyping \noindent 这是栈溢出错误。因为 \type{foo} 函数是无穷递归,而 Lua 解释器在受到无有穷尽的 \type{foo} 函数的折磨时,会选择玉石俱焚,而不是像 \TEX\ 那样受困于这种折磨无法解脱。 我们也可以通过让 \type{foo} 函数讲述宇宙的秘密的方式,拯救 Lua 解释器。例如 \startLUA \startluacode function foo(x) if x == 42 then context("宇宙的秘密是 %d。", x) else foo(x + 1) end end \stopluacode \stopLUA \noindent 然后只需像下面这样调用 \type{foo} 函数便可得到宇宙的秘密: \startTEX \ctxlua{foo(0)} \stopTEX \CONTEXT\ 命令 \tex{ctxlua} 与 Lua 代码里的 \type{context} 函数,你可以将其理解为 \CONTEXT\ 世界与 Lua 世界的通道的两端。有了这一通道,你可以用 Lua 语言编写程序,然后在 \CONTEXT\ 世界里调用它们。 \subject{结语} 也许你本身已是一位训练有素的软件开发者,熟悉许多编程语言,即便如此,当你第一次瞥到 \TEX\ 的宏编程机制时,大概印象是,\TEX\ 作为排版软件可谓优秀,但作为编程语言可谓丑陋不堪。当你写的一个宏出错时,也许会觉得自己面临了一场雪崩,没有一片雪花是无辜的。好在 Lua\TEX\ 已问世十余年了,现已进化为更为轻盈的 LuaMeta\TEX。在你厌恶宏编程时,总是可以考虑用 Lua 语言编写程序,而 \TEX\ 的宏可以作为界面。不过,我的能力只是为你打开一个通向 \TEX\ 幽深世界的入口。至于你在这个世界里能够走多远,会遭遇什么——那是我看不到的,希望会是一段传奇。