Open SwitWu opened 2 months ago
可以看另一个例子:
\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 withine
andx
expansions.
不可完全展开具有传染性:\setCJKmainfont
是不可完全展开的,于是用到它的 \use_cjk_font_fandol:
也不是(需要定义为 protected),于是用到 \use_cjk_font_fandol:
的 \use_cjk_font:
也不是(需要定义为 protected)。
TeX-SX 上有不少关于相关问答,可以搜索、浏览了解
以及为什么这个方式能避免是
\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
\protected
prefix) are not expanded when building an expanded token list (for\edef
,\xdef
,\message
,\errmessage
,\special
,\mark
,\marks
or when writing the token list for\write
to a file) or when looking ahead in an alignment for\noalign
or\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 ... \endcsname
command 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).
多嘴说一句:问「为什么」的时候,可以简单描述当前自己的理解,这样回答方可以有的放矢,否则回答方(至少我会这么想)会不知道是哪里没通,会怀疑自己是不是要从头到尾、事无巨细地描述一遍。有时候会详细描述,有时候因为时间精力和情绪限制,会起到反作用、直接不回复。答的一方总是优先用自己的思路解释,如果问的一方是在不同的思路上卡住,这时问的一方主动指出思路的差异、自己的卡点,也会提高交流效率。
非常感谢!!
检查清单
操作系统
macOS Sonoma 14.4.1
TeX 发行版
TeX Live 2024
描述问题
本问题是关于 l3keys 模块中
.initial:n
的作用机制,我们考虑下面的最小工作示例:pdflatex
输出结果为a
,这个结果还是符合预期的。下面将两个\cs_new:Npn
挪到\keys_define
的后面,即输出结果为
empty
。为什么在这种情况下.initial:n
的设置不起作用?最小工作示例(MWE)
上面已给出
链接
No response
其他信息
No response
附件
No response