网站首页 > 基础教程 正文
话题:#科技# #数学# #计算机语言# #LaTeX#
小石头/编
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编程的更多内容,后续文章再讨论!)
猜你喜欢
- 2025-05-08 最全的 Vue 面试题+详解答案(vue的一些面试题)
- 2025-05-08 Array类型简介(array常用的几种方法)
- 2025-05-08 2021最全VUE面试题,奥利给(vue 面试题 2021)
- 2025-05-08 Vue3基础难点总结(vue3技巧)
- 2025-05-08 使用Axios 拦截器解决「 前端并发冲突 」 问题
- 2025-05-08 浅析vue封装自定义插件(vue自己封装组件)
- 2025-05-08 perl基础——数组(perl数组初始化)
- 2025-05-08 如何在控制台打印出一个表格?cli-table3了解一下
- 2025-05-08 ES6史上最全数JS数组方法合集-02-数组操作
- 2025-05-08 3个 Vue $set 的应用场景(vue中的set方法)
- 最近发表
-
- 掌握SpringBoot-2.3的容器探针:实战篇
- kubernetes基础知识之驱逐节点(k8s驱逐节点后恢复)
- Linux环境中制作网络哨兵Sentinel Docker镜像
- k8s之配置CNI网络(k8s 网络配置)
- docker实战之:镜像更新(docker 镜像升级替换)
- 离线在docker镜像方式部署ragflow0.17.2
- Linux日常小技巧Docker打包(docker打包lnmp)
- 使用dockerfile构建docker镜像(docker通过dockerfile构建镜像命令)
- 「云原生」Containerd ctr,crictl 和 nerdctl 命令介绍与实战操作
- Kylin安装Dify(kylin安装部署)
- 标签列表
-
- jsp (69)
- pythonlist (60)
- gitpush (78)
- gitreset (66)
- python字典 (67)
- dockercp (63)
- gitclone命令 (63)
- dockersave (62)
- pythonif (68)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- console.table (62)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)