forum
forum copied to clipboard
`\keys_define:nn` 中 `.initial:n` 失效的问题
检查清单
- [X] 我已在 issues 中进行搜索(包括已关闭的问题)
操作系统
macOS Sonoma 14.4.1
TeX 发行版
TeX Live 2024
描述问题
本问题是关于 l3keys 模块中 .initial:n 的作用机制,我们考虑下面的最小工作示例:
\documentclass{article}
\begin{document}
\ExplSyntaxOn
\cs_new:Npn \test_a:
{
\tl_set:Nn \l_tmpa_tl { a }
}
\cs_new:Npn \test_b:
{
\tl_set:Nn \l_tmpa_tl { b }
}
\keys_define:nn { module }
{
test .choices:nn =
{
a , b
}
{
\use:c { test_#1: }
},
test .initial:n = a
}
\tl_if_empty:NTF \l_tmpa_tl { empty } { \l_tmpa_tl }
\ExplSyntaxOff
\end{document}
pdflatex 输出结果为 a,这个结果还是符合预期的。下面将两个 \cs_new:Npn 挪到 \keys_define 的后面,即
\documentclass{article}
\begin{document}
\ExplSyntaxOn
\keys_define:nn { module }
{
test .choices:nn =
{
a , b
}
{
\use:c { test_#1: }
},
test .initial:n = a
}
\cs_new:Npn \test_a:
{
\tl_set:Nn \l_tmpa_tl { a }
}
\cs_new:Npn \test_b:
{
\tl_set:Nn \l_tmpa_tl { b }
}
\tl_if_empty:NTF \l_tmpa_tl { empty } { \l_tmpa_tl }
\ExplSyntaxOff
\end{document}
输出结果为 empty。为什么在这种情况下 .initial:n 的设置不起作用?
最小工作示例(MWE)
上面已给出
链接
No response
其他信息
No response
附件
No response
可以看另一个例子:
\documentclass{article}
\begin{document}
\ExplSyntaxOn
\use:c { test_a: }
\cs_new:Npn \test_a:
{
\tl_set:Nn \l_tmpa_tl { a }
}
\cs_new:Npn \test_b:
{
\tl_set:Nn \l_tmpa_tl { b }
}
\tl_if_empty:NTF \l_tmpa_tl { empty } { \l_tmpa_tl }
\ExplSyntaxOff
\end{document}
结果是 empty
而
\documentclass{article}
\begin{document}
\ExplSyntaxOn
\test_a:
\cs_new:Npn \test_a:
{
\tl_set:Nn \l_tmpa_tl { a }
}
\cs_new:Npn \test_b:
{
\tl_set:Nn \l_tmpa_tl { b }
}
\tl_if_empty:NTF \l_tmpa_tl { empty } { \l_tmpa_tl }
\ExplSyntaxOff
\end{document}
的结果为
! Undefined control sequence.
l.8 \test_a:
?
所以问题应该出在 \use:c <cs> 上,当 cs 未定义的时候,并不会报错。用 unravel 宏包查看展开情况:
|> \use:c {test_a:}
[===== Step 1 =====] \use:c = \long macro:#1->\cs:w #1\cs_end:
||
|> \cs:w test_a:\cs_end:
[===== Step 2 =====] \cs:w = \csname
|| \cs:w
|> test_a:\cs_end:
[===== Step 3 =====] \cs:w test_a:\cs_end: =\test_a:
||
|> \test_a:
[===== Step 4 =====] \test_a: = \relax
||
|>
[===== End =====]
可以看到最后 \test_a: 变成 \relax 了,不会报错。
这是 \csname ...\endcsname 的局限/特性。
所以 eTeX 增加了 \ifcsname,\ifcsname ...\endcsname ... [\else ...] \fi,这时拼出来的命令如果尚未定义,不会被 let 到 \relax。\ifcsname 在 LaTeX3 的别名是 \if_cs_exist:w。
在 LaTeX3 的 \cs_if_exist:(N|c)TF 和 \cs_if_free:(N|c)TF 里,undefined 和 defined but equal to \relax 都视为 non-existed/free。见 https://github.com/latex3/latex3/issues/439 。
一些老的包(在 eTeX 被广泛使用/纳入主流引擎之前就诞生的),会在(不需要完全可展开时)使用特别的技巧来避免未定义的命令被 \csname let 为 \relax,见 https://tex.stackexchange.com/q/47804 .
那这个特性现在需要“避免”吗
@muzimuzhi @xkwxdyy 感谢讨论,这个问题来源于武大论文模版的设置,我将其抽象为下面的 MWE:
\documentclass[fontset=none]{ctexbook}
\ExplSyntaxOn
\keys_define:nn { module }
{
cjk-font .choices:nn =
{ fandol, mac }
{
\cs_gset_eq:Nc \use_cjk_font: { use_cjk_font_\l_keys_choice_tl : }
},
cjk-font .initial:n = fandol,
}
\cs_new:Npn \use_cjk_font_fandol:
{
\setCJKmainfont { FandolSong }
}
\use_cjk_font:
\ExplSyntaxOff
\begin{document}
你好世界
\ExplSyntaxOn
\cs_meaning:N \use_cjk_font:
\ExplSyntaxOff
\end{document}
由于 \use_cjk_font_fandol: 的定义在 .initial:n 的后面,所以导致 \use_cjk_font: 被定义为 \relax (见下图):
改正的方法就是将
.initial:n 放到 \cs_new 的后面,即
\documentclass[fontset=none]{ctexbook}
\ExplSyntaxOn
\keys_define:nn { module }
{
cjk-font .choices:nn =
{ fandol, mac }
{
\cs_gset_eq:Nc \use_cjk_font: { use_cjk_font_\l_keys_choice_tl : }
},
}
\cs_new:Npn \use_cjk_font_fandol:
{
\setCJKmainfont { FandolSong }
}
\keys_define:nn { module }
{
cjk-font .initial:n = fandol
}
\use_cjk_font:
\ExplSyntaxOff
\begin{document}
你好世界
\ExplSyntaxOn
\cs_meaning:N \use_cjk_font:
\ExplSyntaxOff
\end{document}
当然,也可以将 .initial:n 替换为 \keys_set:nn 的等价形式:
\documentclass[fontset=none]{ctexbook}
\ExplSyntaxOn
\keys_define:nn { module }
{
cjk-font .choices:nn =
{ fandol, mac }
{
\cs_gset_eq:Nc \use_cjk_font: { use_cjk_font_\l_keys_choice_tl : }
},
}
\cs_new:Npn \use_cjk_font_fandol:
{
\setCJKmainfont { FandolSong }
}
\keys_set:nn { module }
{
cjk-font = fandol
}
\use_cjk_font:
\ExplSyntaxOff
\begin{document}
你好世界
\ExplSyntaxOn
\cs_meaning:N \use_cjk_font:
\ExplSyntaxOff
\end{document}
输出结果:
按照(至少是我的)习惯,.initial:n 一般就是在 \keys_define:nn 的键值刚定义后就接着写了,也就是第一种比较少见,所以一般是采用你的第二种也就是 set 的方式进行。
不过我觉得 cs 和 variable 能保证先 new 再 use 就行了。
那这个特性现在需要“避免”吗
c-type expansion 得到的 latex3 variable,应该保证已经 new 过了。c-type expansion 得到的 latex3 function,大部分时候都应该先定义再使用。确实需要的,可以用\if_cs_exist:w代替c-type expansion,判断一个拼出来的控制序列是否已定义。
题主的具体例子里,调整 \use_cjk_font: 的定义方式,可以避免使用 \relax。把
\cs_gset_eq:Nc \use_cjk_font: { use_cjk_font_\l_keys_choice_tl : }
改为
\cs_gset_protected:Npe \use_cjk_font: { \use:c { use_cjk_font_\l_keys_choice_tl : } }
同时定义 \use_cjk_font_fandol: 为 protected(\setCJKmainfont 不可完全展开,因为至少有 key-value 设置和 \font 都不可完全展开)
\cs_new_protected:Npn \use_cjk_font_fandol: {...}
这样 \use:c { use_cjk_font_\l_keys_choice_tl : } 得到的要么是 \relax 要么是 protected 的 latex function,它们都不可展开。cjk-font .initial:n = fandol 相当于
\protected\gdef\use_cjk_font:{\use_cjk_font_fandol:}
或者可以在 cjk-font .choices:nn 里只把 \l_keys_choice_tl 储存到全局变量里,不修改 \use_cjk_font:,然后把 "fandol" 到 \use_cjk_font_fandol: 的转换放在 \use_cjk_font: 里。
补充:python 内置库 argparse 的实现很类:定义命令行选项、解析选项、返回一个存储了所有 key-value pair 的对象,这个对象可以很容易地转换为 dict,https://docs.python.org/3/library/argparse.html#the-namespace-object 。
那这个特性现在需要“避免”吗
c-type expansion 得到的 latex3 variable,应该保证已经 new 过了。c-type expansion 得到的 latex3 function,大部分时候都应该先定义再使用。确实需要的,可以用\if_cs_exist:w代替c-type expansion,判断一个拼出来的控制序列是否已定义。题主的具体例子里,调整
\use_cjk_font:的定义方式,可以避免使用\relax。把\cs_gset_eq:Nc \use_cjk_font: { use_cjk_font_\l_keys_choice_tl : }改为
\cs_gset_protected:Npe \use_cjk_font: { \use:c { use_cjk_font_\l_keys_choice_tl : } }同时定义
\use_cjk_font_fandol:为 protected(\setCJKmainfont不可完全展开,因为至少有 key-value 设置和\font都不可完全展开)\cs_new_protected:Npn \use_cjk_font_fandol: {...}这样
\use:c { use_cjk_font_\l_keys_choice_tl : }得到的要么是\relax要么是 protected 的 latex function,它们都不可展开。cjk-font .initial:n = fandol相当于\protected\gdef\use_cjk_font:{\use_cjk_font_fandol:}
您说的这个方式是仍然像题主那样保持 def 在 key set 的后方吗?如果是 def 在前面的话您这个操作是为啥呢,为什么要设置为 protected 以及为什么这个方式能避免是 \relax 呢?不是特别明白。
防止所指不同,先贴对应 https://github.com/CTeX-org/forum/issues/314#issuecomment-2067726586 描述的完整例子:
% !TeX program = xelatex
\documentclass[fontset=none]{ctexbook}
\ExplSyntaxOn
\keys_define:nn { module }
{
cjk-font .choices:nn =
{ fandol, mac }
{
\cs_gset_protected:Npe \use_cjk_font:
{ \use:c { use_cjk_font_\l_keys_choice_tl : } }
},
cjk-font .initial:n = fandol,
}
\cs_new_protected:Npn \use_cjk_font_fandol:
{
\setCJKmainfont { FandolSong }
}
\use_cjk_font:
\ExplSyntaxOff
\begin{document}
你好世界
\ExplSyntaxOn
\cs_meaning:N \use_cjk_font:
\ExplSyntaxOff
\end{document}
您说的这个方式是仍然像题主那样保持 def 在 key set 的后方吗?
这么修改后,cjk-font .initial:n 和 \use_cjk_font_fandol: 的定义是顺序无关的。
- 原来顺序有关,是因为
cjk-font .initial:n = fandol->\cs_gset_eq:Nc \use_cjk_font: { use_cjk_font_\l_keys_choice_tl : }->\let \use_cjk_font_fandol: = \relax \global \let \use_cjk_font: \relax(定义\use_cjk_font:时用到了\use_cjk_font_fandol:的值)- 定义
\use_cjk_font_fandol: - 使用
\use_cjk_font:,它现在是\relax
- 现在顺序无关,是因为
cjk-font .initial:n = fandol->\cs_gset_protected:Npe \use_cjk_font: { \use:c { use_cjk_font_\l_keys_choice_tl : } }->\let \use_cjk_font_fandol: = \relax \gdef \use_cjk_font: { \use_cjk_font_fandol: }(定义\use_cjk_font:时只使用了\use_cjk_font_fandol:这个 token,它的值要等到使用\use_cjk_font:时才会用到) 因为\relax是不可展开的(\edef\x{\relax}与\def\x{\relax}效果相同),所以 let 到\relax的宏也不可展开,\xdef\x{\use_cjk_font_fandol:}与\gdef\x{\use_cjk_font_fandol:}效果相同。- 定义
\use_cjk_font_fandol: - 使用
\use_cjk_font:,它展开到\use_cjk_font_fandol:,后者继续展开到当前的定义
可以用 unravel 逐步查看。
如果是 def 在前面的话您这个操作是为啥呢,
没什么为啥,因为看起来题主想要顺序无关,我就给了一个方案。
Update: 回看之前的所有回复,看起来题主只是在问「为什么代码顺序影响代码行为」,没有倾向于某个顺序。Anyway,就当我 for fun。
在 cjk-font 这个 choice key 里唯一必要的操作是(全局)储存传入的 value,所以我后来又添加了 https://github.com/CTeX-org/forum/issues/314#issuecomment-2067728328 的办法。
为什么要设置为
protected
因为本来就应该 protected。expl3 要求(也许它的文档不够强调这点)所有的 function 要么是 fully expandable 的要么是 protected 的。
见 texdoc interface3(texdoc v4.1 起 (https://github.com/TeX-Live/texdoc/commit/de1ebc7919e3a505988b2d3184df79ff1aec777e),texdoc expl3 也是打开 interface3.pdf),sec. 4.3 "Control sequences and functions"
Functions which are not “protected” are fully expanded inside an
e-type orx-type expansion. In contrast, “protected” functions are not expanded withineandxexpansions.
不可完全展开具有传染性:\setCJKmainfont 是不可完全展开的,于是用到它的 \use_cjk_font_fandol: 也不是(需要定义为 protected),于是用到 \use_cjk_font_fandol: 的 \use_cjk_font: 也不是(需要定义为 protected)。
TeX-SX 上有不少关于相关问答,可以搜索、浏览了解
- fully expandable,如 https://tex.stackexchange.com/q/66118
- protected,如 https://tex.stackexchange.com/a/633600
以及为什么这个方式能避免是
\relax呢?
见前文第一条引用-回复。
最后,(至少和我)不用使用「您」。
Protected function 就是一个定义时使用了(eTeX 增加的)prefix \protected 的宏,例如 \protected\def\x{...} 定义的 \x 就是 protected 的。\long 和 \global 是既有的 prefixes。
texdoc etex, sec. 3.12 "Expandable Commands"
Protected macros (defined with the
\protectedprefix) are not expanded when building an expanded token list (for\edef,\xdef,\message,\errmessage,\special,\mark,\marksor when writing the token list for\writeto a file) or when looking ahead in an alignment for\noalignor\omit.
把所有 non-fully expandable 都定义为 protected,可以不用手动控制在 fully expand 时(如引用的 eTeX manual 里提到的 primitives 和 \expanded,后者对应 expl3 里的 e-type expansion)的展开。Protected function 相当于总是前面带着一个 \noexpand。
texdoc expl3.pdf, sec. 4 "Expansion control" 里从 argument expansion 的角度有所介绍。
这是
\csname ...\endcsname的局限/特性。
见 texdoc texbytopic, sec. 12.5.1 "\relax and \csname".
If a
\csname ... \endcsnamecommand forms the name of a previously undefined control sequence, that control sequence is made equal to\relax, and the whole statement is also equivalent to\relax(see also page 116).
多嘴说一句:问「为什么」的时候,可以简单描述当前自己的理解,这样回答方可以有的放矢,否则回答方(至少我会这么想)会不知道是哪里没通,会怀疑自己是不是要从头到尾、事无巨细地描述一遍。有时候会详细描述,有时候因为时间精力和情绪限制,会起到反作用、直接不回复。答的一方总是优先用自己的思路解释,如果问的一方是在不同的思路上卡住,这时问的一方主动指出思路的差异、自己的卡点,也会提高交流效率。
非常感谢!!