话题:#科技# #数学# #计算机语言# #LaTeX#
小石头/编
经常写论文的各位,即便是没有用过,也听说过 LaTeX 的大名。它其实是 TeX 的二次开发,相当于一个框架或类库,而 TeX 则是一门名副其实的编程语言,只不过它编译生成的是 用于打印或出版的 排版文件 而非 可执行程序。
不论你安装的是 MiKTeX 还是 TeX Live,都可以在安装目录内找到名为 tex 的可执行文件,这就是 TeX 最初的编译器,它的编译结果是 dvi(device independent) 格式文件,之后可以通过 转化程序 将其 转为 pdf、ps、html 等其它格式。虽然,之后又出现了更多的编译器,例如:可直接输出 pdf 格式 的 pdftex,但是 我们今天,只讨论 tex。
高德纳 选择 technology(技术) 的前缀 tech- 对应的 希腊词根的大写形式 TeX 作为名称,是因为 其还含有 art(艺术)之意,同时 用 π(目前是: 3.14159265) 作为 其编译器 的 版本号 还预示着 math(数学)。发明人眼里:
而 TeX 的第一个应用—— 发明人自己的名著: 《计算机程序设计的艺术》,正是上式的完美体现。
TeX 是一个图灵完备语言,也就是说,它与 C/C++、Python、Java 等 这些语言,并无差异,本文的主要目的就是向大家揭示这一少有人使用的特性,而关于如何使用 LaTeX 来排版,大家应该已经很熟悉了,所以这并不在我们今天的讨论范围内。
首先,TeX 是宏语言,这里的宏是完整的,是如Lisp那样的,功能强大的宏, 而非像C语言那种阉割版。其次,TeX是纯粹的宏语言,也就是说,TeX只有宏,而不像Lisp 和 C 那样,除了宏还有函数。
作为传统,让我们用宏,写下第一个TeX程序,吧!
\def\main{Hellow world!}
\main
在这段程序中,我们通过 \def 命令 定义了 一个 名为 \main 的宏,并且在之后 使用它,此时 TeX 会将 \main 替换为 Hellow world!(这称为 宏展开),然后 输出,于是我们就看到这样的一页纸:
将上面代码保持在main.tex的文件中,接着 你有两种运行TeX代码的方式,
1.可以从控制台,运行tex文件,当出现 双星号 (**) 提示符时,输入main.tex 回车,然后出现 单星号(*) 提示符,敲入 \end 告诉TeX,输入结束,这样TeX会生产 main.dvi 文件作为编译结果,可用 yap 等软件查看 dvi 文件。
2.如果有 TeXworks 等 IDE,可以用 IDE 打开 main.tex,将 选择 IDE 的编译器为 TeX(或 兼容 TeX 的 pdfTeX),点击运行,然后在IDE的命令行中输入\end 回车,IDE 会生产 main.div (或 main.pdf)并自动打开。
当然,你也可以让 main.tex 为空,然后 在敲入 \end 之前,将以上代码 逐行敲入!
TeX的宏还可以带有参数,例如:
\def\hello#1{Hello #1!}
\hello U
宏命令的格式是:
\def<命令名><参数文本>{<替换文本>}
每个宏最多可以代九个参数,分别用 #1 到 #9 命名,参数在参数文本中 只能出现一次,而且 必须按照顺序,从 #1 开始排列,用多少使用多少,而在替换文本 可以 出现多次并且顺序不限。
TeX 在展开 \hello U 时,参数#1 会匹配 字符 U,之后 会将 替换文本 Hello #1! 中的参数#1用 对应的 匹配 U 替代,得到 Hello U!,最后再用 该结果 替代 \hello U。
当参数文本中只有,参数序列时,TeX会做无定界匹配,每个参数只能匹配 一个字符,例如:
\hello world
在展开时,#1 匹配 w,于是 \hello w 被展开为 Hello w! 被输出,而之后的 orld不受影响继续输出,最后的结果是:Hello w!orld。为了解决这个问题,可以将多个字符 则需要用 { }将其包起来,形式一个 分组,例如:
\hello {world}
这样,#1 匹配的将是 一个分组,其内容是 world,于是 \hello {world} 也就被展开为 Hello world! 了。
另外,一个解决方案是在参数序列中插入定界符(定界符,一般是类型码为 12 或 11 的字符序列),这时TeX会做模式匹配,每个参数会匹配 一个字符序列,例如:
\def\hello#1.{Hello #1!}
\hello world. % => Hello world!
注:在 TeX 中 % 表示注释,本文中,用 => 表示 输出结果。
这里的定界符 . 使得参数 #1 匹配到字符序列 world,于是自然会得到我们要的 输出结果。
值得一提的是,模式匹配允许参数匹配到空字符序列,例如:
\hello . % => Hello !
当然,也可以只有定界符,没有参数,例如:
\def\th:{{\bf Theorem:}}
\th: It states that $a^2+b^2=c^2$ in a right triangle, $c$ is the side that is opposite the right angle, $a$ and $b$ are the sides that are adjacent to the right angle.
其中,\bf 是 粗体命令,而 $ $ 之间的部分是数学公式。
TeX 将 字符被分为 16 个种类(category),每个字符都有一个类型码(从 0 到 15)表示它的类型,我们可以通过 \catcode<字符编码> (这里的字符编码指的是 字符的 ASCCII 码,TeX并不支持 UTF-8 等) 命令 来查看 任意符号的 类型码,例如:
* \showthe\catcode`a
> 11.
* \showthe\catcode`0
> 12.
* \showthe\catcode`+
> 12.
这里,命令 \showthe <参数> 会将 参数 的 内容显示在 控制台上 并以 . 结尾。而 `<字符> 用于得到 字符的编码
注意:当你在单星号(*) 提示符 下,敲入一个命令 后,会出现 问号(?)提示符,这时需要 敲一个 回车,退回 单星号(*) 提示符,然后才能 敲入下一个命令。
TeX 正是根据 字符的 类型码 来判读 字符是啥的,例如:因为 % 的类型码是 注释符(14),所以 % 被按照注释 处理,因为 { 和 } 的 类型码是 分别是 组开始(1)和 组结束(2),所以 { } 包起来的文本,才能形成组。
任何字符对应的 类型码 不是固定的,我们可以随时通过 \catcode<字符编码>=<类型码> 改变任意字符的类型码,例如:
* \showthe\catcode`@
> 12.
* \catcode`@=11
* \showthe\catcode`@
> 11.
* \catcode`@=\catcode`+
* \showthe\catcode`@
> 12.
一般来说:
- 类型是 字母(11)和 其它字符(12)的字符 会被 TeX 直接排版输出;
- 多个 空格(10)会输出一个空格;
- 单个的 换行(5) 等同于 空格,连续两个 换行 被转为 分段 \par 命令;
但是若字符之前惯有 转义符(0), 则会成为 命令名(TeX 称 命令 为 控制序列,不过本会还是称命令)。
默认情况下,字符 \ 被设置为转义符,当然你也可以指定其它转义符(通过将 该字符的 类型码 设置为 0),但不建议这么做。
TeX中有两种 命令名,
- \ <连续的字母>,例如,前面的 \def,\main,\catcode 等;
- \<单个非字母>,例如,\0, \+ 等;
这里仅仅是名字不同,而它们作为命令被使用时,并无多大区别。
默认情况,字母只有 A-Z 和 a-z,但有时候我们需要一些内部自己用的 命令,这时可以临时让一些 其它字符(12)充当字符,形成内部命令名,例如:
\def\private:{\catcode`@=11}
\def\public:{\catcode`@=12}
\private:
\def\@hello#1{Hello #1!}
\def\main{\@hello{world}}
\public:
\main
的当然,外部用户也不是不可以调用 这些内部命令,除了使用上面的的 \private: 命令外,还可以使用 \csname<名字文本>\endcsname,例如:
\csname @hello\endcsname U
TeX还是一门动态语言,它本身可以看成运行TeX代码的虚拟机,与真实的计算机类似,TeX提供了许多类型的寄存器,以及与之相关的操作。
256个计数寄存器是我们最常用到的,可以通过 \count<编号> 存取它们,例如:
* \count0=7
* \showthe\count0
> 7.
计数寄存器编号是0到255之间的整数,而计数寄存器是32位整数。TeX中数字格式,可以是十进制、八进制、十六进制,另外字符码也是整数,因此,下面的四个赋值结果一样:
\count109=110 %十进制
\count'155='156 %八进制
\count"6D="6E %十六进制
\count`m=`n %字符码
除了是立即数外,整数还可以从计数寄存器中获取,于是,下面的代码和上面的赋值结果也一样:
\count0=109 \count1=110
\count\count0=\count1
TeX提供了对于计数寄存器四则运算:加减 \advance 、乘 \multiply、除 \divide ,它们的格式都是:
- <运算> <计数寄存器> by <数字>;
例如:
% (1+2)*3/5-7 = -6
\count0 = 1
\advance \count0 by 2
\multiply \count0 by 3
\divide \count0 by 5
\advance \count0 by -7
\the \count0 % => -6
其中,命令 \the 和 \showthe 类似,只不过 它将 \count0 内容输出到 排版文件,而非 控制台。
注:赋值中的 = 和 四则运算中的 by ,都可以省略,但是为了清晰建议保留。
有了以上这些基础,我们就可以很方便的实现 后继(也就是 +1)运算,
\def\suc#1{
\count0=#1
\advance\count0 by 1
\the\count0}
这里的运算的结果是直接输出到排版文件,若其它程序需要使用该结果,则必须将其返回。仿照 汇编语言的过程返回值,总是存在 eax 寄存器中,我们可以使用 TeX的 \newcount 命令,让其为我们分配一个 计数计算器,做 eax,具体代码如下:
\newcount\eax
这样一来,后继运算可改写为:
\def\suc#1{
\count0=#1
\advance\count0 by 1
\eax=\count0}
为了更像 其它语言一点,我们还可以给点语法糖:
\def\return#1{\eax=#1}
\def\suc#1{
\count0=#1
\advance\count0 by 1
\return\count0}
然后,试一试 ,
* \suc0 \suc\eax \suc\eax \showthe\eax % (1)
> 3
OK!
\newcount 命令可以避免 保存返回值的 \eax 寄存器 被 重用,但是作为 \suc 内部临时使用的 \count0 寄存器 如何避免被 重用呢?
同样 使用 \newcount 命令,并不现实,因为 我们可能 会写 很多宏,每个宏内部临时使用的 计数寄存器 个数 也不一定是一个,而 \newcount 能分配的 新 计数寄存器 最多 只能有 256 个。
为了解决这个问题,TeX 引入了 分组机制,分组可以嵌套,类似 其它语言的 作用域。如下图,
TeX在初始运行时,会创建一套寄存器组 R?,作为当前寄存器组,供顶层代码使用;
- 当进入1层分组时,TeX会从当前寄存器组 R? 复制一套寄存器组 R?,作为新的当前寄存器组,供分组中的 代码使用;
- 当进入2层分组时,TeX会从当前寄存器组 R? 复制一套寄存器组 R?,作为新的当前寄存器组,供分组中的 代码使用;
- ... ...
- 当退出2层分组时,会销毁当前寄存器组 R?,将当前寄存器恢组复为 R?;
- 当退出1层分组时,会销毁当前寄存器组 R?,将当前寄存器恢复为组 R?;
因此,我们可以在 \suc 中加入分组,使得 \count0 在 R??? 中 ,不影响 调用 \suc 时 R? 中 的 \count0,具体代码如下:
\def\suc#1{{
\count0=#1
\advance\count0 by 1
\return\count0}}
但是,这样会有一个新问题:因为 \eax 也会处于 R??? 中,从 \suc 返回后, R? 中 的 \eax 没有任何改变!为了解决这个问题,TeX 提供了一个 命令前缀 \global<命令>,它表示 命令中修改某个寄存器内容是 对 从 R? 到当前寄存器组 中的所有 对应寄存器 进行修改,而不是 只当前寄存器组 中的 对应寄存器。于是,我们只需要修改 \return 就能解决这个问题:
\def\return#1{\global\eax=#1}
再敲入(1) 处的命令,依然OK!
若对上面这套运行机制有疑惑,大家不妨运行 从下面这段代码:
\newcount\var
\def\setvar#1{\var=#1 [set #1]}
\def\thevar{\ \the\var\ }
\def\test#1.{
\setvar 0 \thevar
{ $\{$
\thevar \setvar 1 \thevar
{ $\{$
\thevar \setvar 2 \thevar
#1\var=3 [[set 3]] \thevar
\setvar 4 \thevar
} $\}$
\thevar \setvar 5 \thevar
} $\}$
\thevar \setvar 6 \thevar
}
\test. % => [set 0] 0 { 0 [set 1] 1 { 1 [set 2] 2 [[set 3]] 3 [set 4] 4 } 1 [set 5] 5 } 0 [set 6] 6
\test\global. % => [set 0] 0 { 0 [set 1] 1 { 1 [set 2] 2 [[set 3]] 3 [set 4] 4 } 3 [set 5] 5 } 3 [set 6] 6
至此,我们已经可以有模有样的定义和使用,与 其它语言 的函数 类似 的 宏了,这预示着我们已经踏入了 TeX 编程的大门。
(为了避免篇幅过长,大家看着累,今天就先讨论到这里。关于 TeX 编程,还有很多内容,就留在 续篇中 再与大家讨论!)