CTeX-org / forum

A temporarily alternate forum of `bbs.ctex.org`
https://t.me/chinesetex
Apache License 2.0
210 stars 16 forks source link

`\keys_define:nn` 中 `.initial:n` 失效的问题 #314

Open SwitWu opened 2 months ago

SwitWu commented 2 months ago

检查清单

操作系统

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

xkwxdyy commented 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:

? 
xkwxdyy commented 2 months ago

所以问题应该出在 \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 了,不会报错。

muzimuzhi commented 2 months ago

这是 \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

muzimuzhi commented 2 months ago

一些老的包(在 eTeX 被广泛使用/纳入主流引擎之前就诞生的),会在(不需要完全可展开时)使用特别的技巧来避免未定义的命令被 \csname let 为 \relax,见 https://tex.stackexchange.com/q/47804 .

xkwxdyy commented 2 months ago

那这个特性现在需要“避免”吗

SwitWu commented 2 months ago

@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 (见下图):

image

改正的方法就是将 .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}

输出结果:

image
xkwxdyy commented 2 months ago

按照(至少是我的)习惯,.initial:n 一般就是在 \keys_define:nn 的键值刚定义后就接着写了,也就是第一种比较少见,所以一般是采用你的第二种也就是 set 的方式进行。

不过我觉得 cs 和 variable 能保证先 new 再 use 就行了。

muzimuzhi commented 2 months ago

那这个特性现在需要“避免”吗

题主的具体例子里,调整 \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:}
muzimuzhi commented 2 months ago

或者可以在 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

xkwxdyy commented 2 months ago

那这个特性现在需要“避免”吗

  • 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 呢?不是特别明白。

muzimuzhi commented 2 months ago

防止所指不同,先贴对应 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: 的定义是顺序无关的。

可以用 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 interface3texdoc 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 or x-type expansion. In contrast, “protected” functions are not expanded within e and x expansions.

不可完全展开具有传染性:\setCJKmainfont 是不可完全展开的,于是用到它的 \use_cjk_font_fandol: 也不是(需要定义为 protected),于是用到 \use_cjk_font_fandol:\use_cjk_font: 也不是(需要定义为 protected)。

TeX-SX 上有不少关于相关问答,可以搜索、浏览了解

以及为什么这个方式能避免是 \relax 呢?

见前文第一条引用-回复。

最后,(至少和我)不用使用「您」。

muzimuzhi commented 2 months ago

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 的角度有所介绍。

muzimuzhi commented 2 months ago

这是 \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).

muzimuzhi commented 2 months ago

多嘴说一句:问「为什么」的时候,可以简单描述当前自己的理解,这样回答方可以有的放矢,否则回答方(至少我会这么想)会不知道是哪里没通,会怀疑自己是不是要从头到尾、事无巨细地描述一遍。有时候会详细描述,有时候因为时间精力和情绪限制,会起到反作用、直接不回复。答的一方总是优先用自己的思路解释,如果问的一方是在不同的思路上卡住,这时问的一方主动指出思路的差异、自己的卡点,也会提高交流效率。

xkwxdyy commented 2 months ago

非常感谢!!