在如今的 LaTeX 中写下 \usepackage{expl3},什么也不会被载入。 expl3——由曾被称作「LaTeX3」的项目发展而来的 LaTeX 编程层——早已是格式的一部分。内核源码 latex.ltx 第 1125 行正记录着 Skipping: expl3 code already part of the format;而 expl3.sty 在第 53 行检查代码是否已经存在,若在,就把整个载入动作直接丢弃。本页会实测 \ExplSyntaxOn 究竟如何改变字符的读法,再依次讲解 \module_function:nn 这类独特名字的读法、参数指定符,以及 tl、seq、prop、int、fp 等数据类型。
expl3 早已在内核之中——\fpeval 就是证据
TeX 本是宏处理器,是用 \def 或 \newcommand 定义命令的工具。但一旦要写大型宏包,原始原语的不一致就暴露出来:控制展开和处理变量成了手艺活。expl3 给 TeX 与 e-TeX 的原语重新命名,系统化地为函数和变量取名,并把每个参数的类型直接写进名字里。它由 LaTeX Project 多年整理而成,既是 LaTeX 的标准库,也是它的编程语言——xparse、siunitx、fontspec、l3keys2e 都建立在它之上。
而且这一层早已不再单独发售。打开 expl3.sty,第 53 至 59 行会检查 \tex_let:D 是否已定义;若已定义,就用 \@gobble 把 \input expl3-code.tex 整个吃掉。作为宏包留下来的只是一个徒有其名的外壳(它的自述也是 L3 programming layer (loader)),实质早已在格式之中。事实上,在什么都不加载的裸 article 里,\ExplSyntaxOn、\tl_new:N、\ProvidesExplPackage 全都已有定义。最直白的证据是 latex.ltx 第 1167 行起的四行——\fpeval、\inteval、\dimeval、\skipeval 用 \cs_new_eq:NN 被定义为 \fp_eval:n、\int_eval:n、\dim_eval:n、\skip_eval:n 的别名。也就是说,文档层的 \fpeval{sqrt(2)} 不过是换个名字在调用 expl3 的函数本身(见「计数器与长度」一页)。
\ExplSyntaxOn 究竟改变了什么:空格与 ~ 的对调
expl3 代码写在 \ExplSyntaxOn 与 \ExplSyntaxOff 之间,而在这一区间里恰好只有 4 个字符的类别码发生变化。让内核打印 \the\catcode 并前后对照,结果是:空格从 10(空格)变为 9(被忽略的字符),_ 从 8(下标)变为 11(字母),: 从 12(其他)变为 11(字母),而 ~ 从 13(活动字符)变为 10(空格)。写下 \ExplSyntaxOff 后,它们会准确地回到 10、8、12、13。类别码本身在「类别码」一页有详述,但这里有意思的是:空格与 ~ 交换了座位。空格让出类别码 10,搬到「被忽略」的 9 号;空出来的 10 号则由 ~ 坐下。所以在 expl3 内部,~ 并不是「输出空格的命令」,它字面上就是一个空格字符。
| 字符 | 普通 LaTeX | ExplSyntaxOn 区间内 |
|---|---|---|
(space) | 10 — 空格 | 9 — 被忽略的字符。 缩进与换行完全不影响输出 |
~ | 13 — 活动字符(不换行空格) | 10 — 空格本身。 需要在输出中留空时就写它 |
_ | 8 — 下标 | 11 — 字母。 可以出现在命令名中 |
: | 12 — 其他字符 | 11 — 字母。 可用作参数签名的分隔 |
这次对调在实务上只意味着一条规则:在 expl3 中写下裸空格,输出里是不会出现的。 想排出「Fruit: apple」,就必须写 Fruit:~#1;把 ~ 换成普通空格,就会粘成「Fruit:apple」。反过来说,你可以随意缩进代码、在记号之间留空以便阅读,而输出丝毫不变。想想普通 LaTeX 对空格有多么敏感,这实在是不小的解放。
% no \usepackage{expl3} is needed: it is in the format
\ExplSyntaxOn
% spaces and newlines here are catcode 9 (ignored); _ and : are letters
\tl_new:N \l_greeting_tl
\tl_set:Nn \l_greeting_tl { Hello,~world! } % ~ is the real space
\tl_use:N \l_greeting_tl
\ExplSyntaxOff如何读懂 \seq_put_right:Nn——名字本身说明类型
expl3 的函数名形如 \⟨module⟩_⟨description⟩:⟨arg-signature⟩。第一个 _ 之前是 模块名(数据类型或功能领域),: 之前是 描述性名称,: 之后是 参数签名。以 \seq_put_right:Nn 为例:模块是 seq(序列),描述是 put_right(追加到右端),签名是 Nn。签名中的每个字母表示 在传入该位置的参数之前要如何处理它。也就是说,读一遍名字就知道它接受几个参数、每个参数会被怎样对待——不必翻手册就能看出类型,这正是这套命名规则的用意。
| 指定符 | 含义 |
|---|---|
N | 不做处理;单个记号(通常是一个控制序列)。 |
n | 不做处理;用花括号包住的记号列。 |
c | 先把内容通过 \csname 转换成控制序列 再使用。 |
V / v | 取出并传递 变量的值(V 来自单个记号,v 先构造名称)。 |
o | 先 展开一次 再传递。 |
x / e | 完全展开(x 类似 \edef,不可展开;e 使用 \expanded)。 |
f | 从左到右展开,直到 第一个不可展开记号 为止。 |
p | TeX 的 参数文本(#1#2…);用于定义函数。 |
T / F | 条件为 真时/假时 执行的代码。通常成对写作 TF,如 \tl_if_empty:nTF;也有只带 T 或只带 F 的版本。 |
变量的命名也遵循同一风格,但开头多出 表示作用域的一个字母。l_ 表示 局部(只在当前 TeX 分组内改变),g_ 表示 全局,c_ 表示 常量。名字末尾是类型标识:_tl(记号列表)、_int(整数)、_seq(序列)、_prop(属性列表)、_clist(逗号列表)、_fp(浮点数)、_str(字符串)、_bool(布尔值)等。因此 \l_my_name_tl 一眼可读为「局部记号列表变量」,\g_counter_int 则是「全局整数变量」。各模块还提供 scratch 变量,即一次性临时变量,例如 \l_tmpa_tl、\l_tmpb_int。
亲眼看清 :Nn 与 :Nx 的区别
n 原样保存你写下的内容,x 则把「此刻」的值烧录进去。 与其抽象地解释,不如用 \tl_show:N 把内容打进日志来得快。把同一个 { [ \l_src_tl ] } 分别用 \tl_set:Nn 和 \tl_set:Nx 存进两个变量,日志会显示:> \l_a_tl=[\l_src_tl ]. 与 > \l_b_tl=[FIRST].。n 版原样持有 \l_src_tl 这个记号,x 版则已经替换成赋值当时的值 FIRST。此后把 \l_src_tl 改成 SECOND 再输出两者,n 版打出「[SECOND]」,x 版打出「[FIRST]」。也就是说,n 保存的是引用,x 保存的是快照。人们所谓「展开控制很难」的场合,多半就是这个选择做反了。\tl_show:N 值得记住,是卡住时的第一招。
\ExplSyntaxOn
\tl_new:N \l_src_tl \tl_set:Nn \l_src_tl { FIRST }
\tl_new:N \l_a_tl \tl_set:Nn \l_a_tl { [ \l_src_tl ] } % reference
\tl_new:N \l_b_tl \tl_set:Nx \l_b_tl { [ \l_src_tl ] } % snapshot
\tl_show:N \l_a_tl % log: > \l_a_tl=[\l_src_tl ].
\tl_show:N \l_b_tl % log: > \l_b_tl=[FIRST].
\tl_set:Nn \l_src_tl { SECOND }
Nn~stored:~\tl_use:N \l_a_tl \par % prints [SECOND]
Nx~stored:~\tl_use:N \l_b_tl \par % prints [FIRST]
\ExplSyntaxOff数据类型:创建、设置、使用
expl3 按数据类型分成多个模块,而它们都遵循同样的节奏:创建、设置、使用。用 \⟨type⟩_new:N 声明,用 \⟨type⟩_set:Nn 放入值,用 \⟨type⟩_use:N 之类取出。类型变了手法不变,所以学会一个就能推测其余。只有函数定义的形式略有不同:\cs_new:Npn 定义新函数,若同名函数已存在则报错(cs 表示 control sequence)。\cs_set:Npn 也定义函数,但只限当前 TeX 分组内,重新定义也不会报错。二者都使用 :Npn:N 是要定义的函数名,p 是参数文本(#1#2…),n 是函数体(替换文本)。
| 模块 | 存放什么 | 常用函数与实测结果 |
|---|---|---|
tl | 记号列表——最基本的变量,可当字符串使用 | \tl_new:N / \tl_set:Nn(旧内容会被丢弃)/ \tl_use:N / \tl_show:N |
str | 字符串——需要按字符而非记号比较时使用 | \str_if_eq:nnTF { a~b } { a~b } 返回 same |
int | 整数——四则与取余的整数运算 | \int_eval:n { 2 + 3 * 4 } 得 14(\inteval 即其别名) |
fp | 浮点数——可用 sin、sqrt、pi 等科学函数 | \fp_eval:n { sqrt(2) } 得 1.414213562373095,\fp_eval:n { 2 * pi } 得 6.283185307179586 |
seq | 序列——可从两端存取的列表,也可用作栈 | \seq_put_right:Nn 追加,\seq_map_inline:Nn 以 #1 遍历全部元素,\seq_use:Nn 用分隔符连接 |
prop | 属性列表——即字典,键到值的映射 | \prop_put:Nnn 存入「变量、键、值」,\prop_item:Nn 取出 |
clist | 以逗号分隔的值序列 | 在 \clist_set:Nn \l_c_clist { 1,2,3 } 之后,\clist_use:Nn \l_c_clist { ~+~ } 得到「1 + 2 + 3」 |
bool | 布尔值——用来替代 \newif | 用 \bool_new:N / \bool_set_true:N / \bool_if:NTF 进行分支 |
\documentclass{article}
\begin{document}
\ExplSyntaxOn
\seq_new:N \l_example_fruits_seq
% define a function that appends one item
\cs_new:Npn \example_add:n #1
{ \seq_put_right:Nn \l_example_fruits_seq {#1} }
\example_add:n { apple }
\example_add:n { banana }
\example_add:n { cherry }
% each item arrives as #1; ~ is a real space
\seq_map_inline:Nn \l_example_fruits_seq
{ Fruit:~#1 \par }
\ExplSyntaxOff
\end{document}编译后会分三行输出「Fruit: apple」「Fruit: banana」「Fruit: cherry」。\cs_new:Npn 中的 #1 是被定义函数的参数,而 \seq_map_inline:Nn 中的 #1 是正在遍历的每个元素;二者都按 n 型(花括号内容)处理。命令名中带 _ 和 : 也无妨,正因为这里是 \ExplSyntaxOn 区间。
l3keys,以及编写宏包时的用法
想给自己的命令或宏包加上 key = value 形式的选项时,标准工具是 l3keys。用 \keys_define:nn { module } { ... } 声明键,用 \keys_set:nn { module } { key = value } 设定。键的声明后面要跟一个 属性,如 .tl_set:N(存入记号列表变量)、.bool_set:N(作为布尔值)、.code:n(执行任意代码)、.initial:n(默认值)。这套机制原本通过名为 l3keys2e 的宏包与宏包选项相连,但其核心如今已被并入 LaTeX2ε 内核,可以不加载任何宏包地以 \DeclareKeys 和 \ProcessKeyOptions 使用(宏包编写一页有实际写法)。
在普通的文档写作中,几乎不需要直接编写 expl3。但一旦要 编写宏包或文档类,expl3 如今已是事实标准。常见的搭配是:用 \NewDocumentCommand(xparse)接收面向用户的命令,再用 expl3 实现其内部。要把两件事分清:xparse 的参数指定(m、O{...}、s 等「文档层」参数)与 expl3 的参数签名(N、n 等「编程层」处理指定)不是一回事。前者在 xparse 一页详述。另外,若 .sty 的第一行写 \ProvidesExplPackage,从那一刻起就自动进入 expl3 语法——完全不必写 \ExplSyntaxOn。
- 代码要包在
\ExplSyntaxOn…\ExplSyntaxOff中。区间内裸空格会变成类别码 9(被忽略);需要在输出中留空时用~。 - 不需要
\usepackage{expl3}——它已在内核中,而expl3.sty会自行丢弃载入动作。在.sty中使用\ProvidesExplPackage,连\ExplSyntaxOn也可省去。 - 函数名为
\⟨module⟩_⟨description⟩:⟨signature⟩;变量名为\⟨scope⟩_⟨name⟩_⟨type⟩,作用域为l_/g_/c_。 - 别把
n和x弄混。 按照你想保存引用还是烧录当前值来选。拿不准就用\tl_show:N打进日志看一眼。 - 不要凭空发明命令名。命名非常严格,官方 interface3 手册(
texdoc interface3)是第一手资料。