专业编程基础技术教程

网站首页 > 基础教程 正文

TeX是门语言(续三)(tem语言)

ccvgpt 2025-05-08 17:05:55 基础教程 1 ℃

话题:#科技# #数学# #计算机语言# #LaTeX#

小石头/编

TeX是门语言(续三)(tem语言)


TeX在原子命令的执行上是严格按照顺序的,但是宏的展开却允许在顺序的基础上进行微调,这样做的因为是:宏展开相当于程序员编写代码的过程,对于程序员来说,只能是大部分时间按顺序coding,但有时会返回修改或暂时跳过。

在前面章节中,我们已经接触过二种调节展开顺序的用例,

  • 保证尾递归:
\def\cs{
   ...
   \else
      ...
      \expandafter \cs
   \fi}

通过 \expandafter 使得 \fi 先展开,而 \fi 展开为空,于是 \cs 就到了自己的尾部。

  • 阻止定义时(高速模式)展开:
\toks77={\cmd}
\edef\cs{
    \noexpand \cmd
    \the\toks77
}

命令 \noexpand 会阻止 \cmd 在\cs的定义时展开,\the会取出\toks77的内容\cmd,但不会做进一步展开。

命令执行优先级,其实也需要改变了 执行顺序,考虑如下命令复合代码,

\def\a#1.{a[#1]} \def\b#1.{b(#1)}
\a \b x.. % => a[b(x])

输出结果出错了!这显然不是命令复合的本意,我们想要的结果是 ①

a[b(x)]

分析原因:第二行,由于 \a 先被执行 所以 \b x 成了它的参数,于是展开为(注意:命令会吃的后面的空格),

a[\b x].

接着\b被展开,x]匹配为它的参数,于是最终展开为,

a[b(x])

要修复这个BUG就要 \b 先于 \a 展开。这在其它 语言里,可以使用 括号 来调整表达式的优先级,但是TeX没有这个机制。不过可以使用 分组来 替代,第二行可改为,

\a {\b x.}.

这次在\a展开时,参数变成了一个分组 {\b x.},于是展开为,

a[{\b x.}]

然后是\b再以x为参数展开,得到 ②,

a[{b(x)}]

最后,执行展开结果,分组符号不会输出,最终结果为就是①了。

这里必须注意,②和① 是有区别的,也即是说,在使用 分组调节优先级的同时引入了分组。若想要展开的直接结果就是 ① ,可以这样,

\expandafter \a \b x..

命令 \expandafter 保持 \b 先于 \a 被展开一次,它匹配参数是 x,于是展开为,

\a b(x).

接着,\a再以 b(x) 为参数,展开就是 ①了。

在一般情况下 ① 和 ② 并差别,可是在 \let 命令中,就会不同。考虑用 \csname命令来产生命令名,例如:

\let\csname zero\endcsname=0 % ! Extra \endcsname.

出错了!原因是 \let 将 \csname 当成了要复制的命令,而由于 = 可是省略,所有实际上是将 字符 z 赋值给了 \csname,之后 ero 不展开,然后到\endcsname 由于找不到 与之匹配的 \csname 于是就多了出来 。

解决这个问题,就是要让 \csname 先于 \let 被执行。此时如果使用 分组,

\let{\csname zero\endcsname}=0 % ! Missing control sequence inserted.

则不行了,原因是分组并不是命令不能被赋值。而使用 \expandafter,

\expandafter\let\csname zero\endcsname=0
\zero % => 0

就没有问题了!

\def 与 \let 类似,例如:

\def\csname zero\endcsname{0}
\zero % ! Undefined control sequence.

出错了,原因是 第一行等于 重新定义了 \csname。解决这个问题,同样需要使用 \expandafter,

\expandafter\def\csname zero\endcsname{0}
\zero % !  % => 0

OK!


直到现在,我们使用宏来生产代码都是,句法级别的,也就是以 标记(token)为单位的,而宏还可以深入到,词法级别,也就是可以分拆和组合命令名。

  • 分拆命令名:

TeX 提供 命令 \string 将 命令 转为 命令名字符串,例如:

\string \cs % => \cs

但是之前会带有 转义符 \,这时可以写一个 单参数 空命令,吃掉它,

\expandafter\tail \string \cs % => cs

然后,将这个功能包成一个宏,

\def\uncsname#1{\expandafter\tail\string #1}
\uncsname\cs % => cs

我们就完成了分拆命令名的第一步,取出命令名。

接下就可以使用带定界符的宏进行模式匹配,进一步分析命令名。

  • 组合命令名:

这个我们已经知道了就是 \csname 命令,有三类对象和相互组合:

⑴ 立即字符:

\def\value{abc}
\expandafter\let\csname abc\endcsname=\value

⑵ 宏:

\def\name{b}
\expandafter\let\csname a\name c\endcsname=\value

⑶ 标记列寄存器:

\toks77={b}
\expandafter\let\csname a\the\toks77 c\endcsname=\value

注:宏命令 \char n 可以产生单个编码是n的字符,而 ^^n 可产生编码是 n±127 的字符。

好了,关于词法宏,的机制也就这么多了,下面举个栗子。

考虑,前文提到过的, TeX 生成标记的命令,

\newif\ifabc 

实际上,TeX仅仅为我们添加如下两行代码 ③,

\def\abctrue{\let\ifabc=\iftrue}
\def\abcfalse{\let\ifabc=\iffalse}

这 功能上很简单,因此要实现 \newif 命令的关键是 从 \ifabc 命令名中 解析出 abc ,再由 abc 组成出\abctrue、\abcfalse 和 \ifabc 三个命令。实现分析:

  • 解析出abc,

考虑 使用 \uncmd 可从 \ifabc 先 中 解析出 ifabc,然后再使用 两次 tail 分别吃掉 if 就剩下了 abc,具体代码如下:

\edef\flag{\uncsname\ifabc}
\edef\flag{\expandafter\tail\flag} 
\edef\flag{\expandafter\tail\flag}
\flag % => abc

值得一提:由于 \expandafter 也是宏,于是可以被自己作用自己 用以达到 排序效果,例如上面的第二、三行代码可以改写为:

\edef\flag{\expandafter\expandafter\expandafter\tail\expandafter\tail\flag}

是不是很酷!

  • 定义命令,

这个并不难,但要搞清,定义时 和运行时,(以 \abctrue 为例)代码如下:

\expandafter\edef\csname\flag true\endcsname{\noexpand\let\csname if\flag\endcsname=\noexpand\iftrue}

试一试,

* \show\abctrue
> \abctrue=macro:
->\let \ifabc =\iftrue 

和 ③ 处一摸一样,OK!

综上,\newif 的最终实现为,

\def\newif#1{
    \edef\flag{\unwrap #1}
    \edef\flag{\expandafter\tail\flag} 
    \edef\flag{\expandafter\tail\flag}
    \expandafter\edef\csname\flag true\endcsname{\noexpand\let\csname if\flag\endcsname=\noexpand\iftrue}
    \expandafter\edef\csname\flag false\endcsname{\noexpand\let\csname if\flag\endcsname=\noexpand\iffalse}
    \abcfalse}

试一试,

\newif\ifabc

\ifabc
    abc true 
\else
    abc false % => abc false
\fi

\abctrue
\ifabc
    abc true % => abc true
\else
    abc false
\fi

OK!

这词法宏看着不难,但是用起来很容易出来,大家使用时一定要谨慎。


在任何语言的使用的讨论中,数据结构都是不可避免的话题,而列表又是数据结构的核心之一。前面讨论过 逗号分隔 的 整数列,这里在我们谈谈 TeX 代码本身,即,标记列,标记列寄存器本来就是用来保存它的,它才是 TeX 的第一等公民,接下来,如果无特殊说明,所谓 列表 就是指它。

借助于前面讨论过的 \edef + \the 的特性,很容易将两个 标记列寄存器 合并起来,放在一个列表宏里,

\edef\c{\the\toks0\the\toks1}

于是要合并两个列表宏,只需要在这之前,将其内容保存在 这两个标记列寄存器中,

\toks0=\expandafter{\a} \toks0=\expandafter{\b} 

其中 \expandafter 确保 \a 被展开,而非以 命令的 形式 呆在 列表中。在这里标记列寄存器仅仅是临时被使用,我们的列表最终都保持在宏中。

将以上代码封装起来就是,

\def\concat#1=#2{
    \toks0=\expandafter{#2} \toks1=\expandafter{#3}
    \edef#1{\the\toks0\the\toks1}}

测试一下,

\edef\a{a} \edef\b{b}
\concat\c=\a&\b 
\c % => ab

OK!

接着就是列表中往添加元素问题。之前的整数列 的元素 之间 是用 逗号分隔的,即,

n1, n2, n3

而这次的是标记列,每个元素item都是(一个或多个)标记,所以应该用 分组来分隔,然后为了将来通过宏参数的模式匹配来处理列表,还需要在每个分组前面再加上\@用来定界,因此我们的列表元素是这样的,

\@{item1}\@{item2}\@{item3}

这样以来,在列表末尾 添加元素的具体代码如下,

\def\append#1\to#2{
    \toks0={\@{#1}} \toks1=\expandafter{#2}
    \edef#2{\the\toks1 \the\toks0}}

测试测试,

\let\lst=\empty
\append a\to\lst \append b\to\lst \append c\to\lst 
\show\lst % -> \@{a}\@{b}\@{c}.

OK! 当然,也允许向列表头添加元素,代码如下,

\def\unshift#1\to#2{
    \toks0={@@{#1}} \toks1=\expandafter{#2}
    \edef#2{\the\toks0\the\toks1}}

测试一下,

\unshift 0\to\lst
\show\lst % -> \@{0}\@{a}\@{b}\@{c}.

OK!

然后,就是取出列表的首项了,代码如下,

\def\shift#1\to#2{
    \def\performshift\@##1##2\to##3##4{\def##3{##1} \def##4{##2}}
    \expandafter\performshift#1\to#2#1}

试一试,

\shift \lst\to\0
\show\0 % -> 0.
\show\lst % ->\@{a}\@{b}\@{c}.

OK!

最后,就是对列表中元素的处理问题了。在之前的整数列中,我们使用了函数式语言的传统方法,即,

  • 把整数列中的每个元素取出来进行处理,然后再将处理结果组成新的整数列 ;

但这里的 标记列,其实就是 TeX 代码,它本身就可以运行的,如果我们用 而我们特意使用了 宏命令 \@ 作为 分界符,它索然在参数匹配中不起作用,但是如果 列表去运行,就会被执行,不就相当于获得了 处理 对每个 元素的能力吗?

按照这个思路,我们只需要,将处理动作提前封装在 \@, 然后让列表宏 执行,就可以了处理列表元素了。例如,计算列表的长度,

\def\length#1\to#2{
    #2=0
    \def\@##1{\advance #2 by 1}
    #1}

测试一下,

\show\lst % ->\@{a}\@{b}\@{c}.
\length\lst\to\eax % => 空
\showthe\eax % -> 3.

完美!

将列表输出成我们熟悉的写法非常容易,

\def\thelist#1{
    \eax=0
    \def\@##1{
        \advance \eax by 1
        \ifnum \eax=1 \else , \fi 
        ##1}
    #1}
\thelist\lst % => a,b,c

OK!

实现map功能同样非常容易,

\def\map#1\by#2\to#3{
    \let#3=\empty
    \def\@##1{
        \edef\maptemp{#2##1} 
        \expandafter\append \maptemp\to#3}
    #1}

测试一下,

\def\uc#1{
    \if a#1 A \else \if b#1 B \else \if c#1 C \else ... \fi\fi\fi}
\map\lst\by\uc\to\a
\show\a % ->\@{  A }\@{  B }\@{  C }.

OK!

注意:由于这个map的实现是,在 定义时 调用 #2 的,所以 #2 中的实现不能使用 非运行时 的原子命令,比如 \advance 等。


(好了,续三就写到这里吧!关于 TeX编程的更多内容,后续文章再讨论!)

Tags:

最近发表
标签列表