这和我之前问的一个问题有点相关这里。
我正在尝试使用分隔符拆分标记。但是,分隔符存储在标记中。查看最新文档,我有以下选项:
\seq_set_split:Nnn
\seq_set_split:NnV
以及相应的全局变体。
现在假设我有以下定义,
\tl_new:N \l__delimiter_tl
\tl_set:Nn \l__delimiter_tl {;}
进一步假设我有
\tl_new:N \l__content_string_tl
\tl_set:Nn \l__content_string_tl {a;b;c;d}
我希望能够写出类似下面的内容:
\seq_new:N \l__possible_values_seq
\seq_set_split:NnV \l__possible_values_seq {\l__delimiter_tl} \l__content_string_tl
但那不起作用。
我也尝试过做类似的事情
\seq_new:N \l__possible_values_seq
\seq_set_split:NnV \l__possible_values_seq {\tl_use:N \l__delimiter_tl} \l__content_string_tl
但这也不起作用。
现在我知道我可以创造一个变体例如
\cs_generate_variant:Nn \seq_set_split:Nnn {NVV}
因为我可以写
\seq_set_split:NVV \l__possible_values_seq \l__delimiter_tl \l__content_string_tl
这样就可以完成任务了。但是,在上面的链接中,有人评论说,写这样的变体不太好。我想遵循该评论的精神。
所以我的问题是,我该怎么做插在读取之前,先检查 token 列表。似乎应该有某种方法可以做到这一点。在 TeX 中,我会这样做
\expandafter\<macro_name> {\<first_argument>}....
然而,我仍在尝试理解 LaTeX3 的语法,似乎 LaTeX3 的优点之一是你可以避免代码难以阅读,例如
\exp_after:wN \exp_after:wN \exp_after \seq_set_split:NnV \exp_after:wN \l__possible_values_seq {\l__delimiter_tl} \l__content_string_tl
顺便说一句,我尝试过,但得到的是垃圾。
那么,在尝试阅读之前,我该如何\l__delimiter_tl
进行扩展呢?\seq_set_split:NnV
编辑
在我之前的帖子的评论中,有人讨论了创建别名对于 LaTeX3 函数。例如,编写类似
\cs_new_eq:NN \seq_length:N \seq_count:N
但在该帖子的评论,有人指出,这将是一个糟糕的命名惯例。我误解了别名和变体?
编辑二
似乎除了创建变体之外,我还可以这样写:
\exp_args:NNVV \seq_set_split:Nnn \l__possible_values_seq \l__delimiter_tl \l__content_string_tl
并避免使用\cs_generate_variant:Nn
。
我知道这\cs_generate_variant:Nn
可能是首选。但似乎可能在某些情况下——例如,在软件包中仅发生一次需要变体的情况——在这种情况下,生成变体可能不太明确。对此有什么看法?
答案1
我认为“变体”的概念expl3
是 LaTeX3 最基本的概念之一,所以我想稍微扩展一下 Joseph 已经给出的答案。
命名约定
LaTeX3 中的命令命名约定 ( expl3
) 将命令名称结构化为
\
模块_
描述:
参数说明符
- 模块标识函数操作或使用的(主要)数据类型(例如,
int
(整数)、prop
(属性列表)等,也可能是包的名称或某些特定概念 - 描述说正在做什么,例如
put_left
,,,,,等。如果它有意义,则get
相同clear
count
描述可以被重复使用,但是对于特殊任务,当然有些只能使用一次。 - 参数说明符最后描述函数有哪些参数以及如何处理它们(更多内容见下文)。
基本功能
基础参数说明符是:
N
表示参数由单个标记组成(不需要括号,有时甚至不允许)n
用括号表示“正常”参数p
#1#2
表示像定义中的参数规范T
就像n
,但告诉程序员这是条件中的“真实”路径F
类似于n
,但在条件中表示“错误”路径
命令参数说明符仅由这些字母组成的基函数。
变体函数
还有更多 参数说明符例如可用
c
将参数解释为字符串并从中生成命令名称V
该论点是多变的,利用其价值v
参数是一个字符串,从中生成一个变量名,然后使用该变量的值o
扩大论点一次像\expandafter{...}
x
扩展参数,就好像它本来位于替换文本中一样\edef
f
扩大论点充分直到你碰到一个不可扩展的标记然后停止扩展
任何使用以下一个或多个命令 参数说明符被称为变体相应的基函数。这里重要的是,所有这些额外的参数说明符以某种方式修改论点,并且只然后将其传递给底层基函数。 例如:
\foo_bar:cVno {cmd} \VAR {text} \CMD
会
- 从字符串生成
cmd
命令名称\cmd
- 查找变量的值
\VAR
text
别管- 展开
\CMD
一次并用括号括起结果
并且仅然后它会调用相应的基函数如下:
\foo_bar:Nnnn \cmd {<value-of-\VAR>} {text} {<one-level-expansion-of-\CMD>}
现在理想情况下我们希望任何可能的变体的基函数程序员可以自动使用。不幸的是,这只有在所有变体都已预定义的情况下才能可靠地完成(因为 TeX 不允许您捕获“未定义的 csname”错误并即时执行某些操作)。
给定参数说明符的数量和预定义所有可能的排列变体,其中 90% 根本不需要,这是不现实的。因此,我们采取了以下策略:
- 概念上全部有多种变体,每个人都可以假设这种情况
- 实际上,内核只定义了一个经常需要的子集
- 任何变体内核没有定义,需要程序员使用以下方式定义
\cs_generate_variant:Nn
\cs_generate_variant:Nn
其设计方式是,即使多次调用也不会有影响:如果变体已经存在,则不会执行任何操作。因此,如果两个程序员在他们的包中定义相同的变体,这不会造成影响,第一个执行的程序员将定义变体,第二个将被忽略(开销很小)。- 如果某些变体使用得相当频繁,它们最终可能已经在内核中定义。由于最后一点,如果某些软件包仍然定义变体,则不会有什么坏处,也就是说,在这种情况下程序员无需修改他们的软件包。
总结一下:每当您需要一个未预定义的变体时,请在代码开头定义它。如果您只需要一次变体,这甚至是明智的,因为使用变体的代码比任何手动预处理的参数更具可读性,并且速度差异接近于零。
功能\exp_args:N...
从技术上讲,通过以下方式定义的变体\cs_generate_variant:Nn
具有非常简单的定义:\foo_bar:cVno
上面的内容将简单地扩展为
\exp_args:NcVno \foo_bar:Nnnnn
因此,不需要定义变量然后像这样使用它:
\cs_generate_variant:Nn \foo_bar:Nnnnn {cVno}
...
\foo_bar:cVno {cmd} \VAR {text} \CMD
当然,也可以直接使用它的替换文本,即
\exp_args:NcVno \foo_bar:Nnnn {cmd} \VAR {text} \CMD
如果你这样做,那么你就是在优化速度(避免了一次扩展 - 好极了),代价是代码可读性降低,除非在非常特殊的情况下,否则这是没用的。内核中有一些命令已经非常仔细地优化了速度,但除非你希望你的命令在文档中被调用数千次,否则根本无法衡量速度的提升。关于清晰度的问题,即使变体在代码中只使用一次,它也会使代码更清晰,因为所有参数说明符都有通用含义,无论基函数做什么,我都可以立即看到参数发生了什么(希望从其模块和描述部分)。
因此总的来说建议是:除非有非常好的理由,否则不要\exp_args:...
在代码中使用。当然,“好”的理由可能是,你想快速测试一些东西,然后投入其中,\exp_args...
这比定义变体并使用它们更快。但这有点像有人说不需要记录这段代码,因为它只是暂时的(... 在接下来的 10 年里)。
为了同样的原因避免\exp_after:wN
!如果您使用它,您通常会在以纯 TeX/LaTeX2e 扩展为基础的编程风格思考时犯一些错误。(那么为什么内核代码有这么多\exp_after:wN
???好吧,我们在那里提供低级机制,某些事情需要发生 :-) 但这不是 LaTeX3 编程所基于的模型。)
其他参数说明符
上面还有许多其他参数说明符我没有提到。它们有点奇怪,并且不支持上述意义上的变体生成。
D
方法不要使用此命令。它仅适用于内核(如果有的话)。这实际上并没有说明此命令是否有参数以及有多少参数。w
意味着这个命令有一个诡异的参数结构通常不会进一步详细说明。例如\exp_after:wN
(在纯 TeX 中称为\expandafter
)有点奇怪,因为它扩展了第二个标记(N
),但第一个标记可以是单个左括号或右括号。
别名即变体
与此相反变体(在将结果传递给基函数之前操纵参数)是别名命令不提供任何新功能。它只是以新名称“复制”的命令。它之所以有用,是因为代码中的抽象(隐藏了实现细节),并且作为副作用,它还提高了可读性。
例如,数据类型seq
(序列)在内部实现为具有tl
一些内部结构的简单标记列表()。因此,“清除”一个序列,即,\seq_clear:N
与清除标记列表完全相同\tl_clear:N
,即后者(目前)也会清除该序列。但在这里提供一个单独的名称要好得多:
- 它使代码更具可读性(你会看到你正在清除一个序列)
- 程序员不需要记住哪些命令特定于序列,哪些命令不特定于序列
- 序列类型有一个适当的抽象:如果实现发生变化,代码不会受到影响。例如,属性列表将实现从
toks
寄存器更改为tl
s。
综上所述,别名只有当您有理由以不同的名称提供现有功能时,您才会想要使用它。
答案2
变体是解决问题的正确方法
\seq_set_split:NVV \l__possible_values_seq \l__delimiter_tl \l__content_string_tl
在执行“base 函数”之前,将扩展两个标记列表\l__delimiter_tl
及其\l__content_string_tl
值(\tl_use:N <name>
将给出的值)\seq_split:Nnn
。由于 LaTeX3 内核代码不会生成每个命令的所有变体,因此您需要
\cs_generate_variant:Nn \seq_set_split:Nnn { NVV }
这里。即使稍后将其添加到内核中,这也是安全的:设计意味着\cs_generate_variant:Nn
它不会覆盖已经存在的变体。
创建等效函数,例如
\cs_new_eq:NN \seq_length:N \seq_count:N
是一个单独的进程:它们都执行完全相同的操作,但名称不同。在这种情况下,您可能需要暂时执行此操作,因为内核代码中有一些重命名。(expl3
处于“大致稳定”状态,因此我们会通知更改,但目前还处于不会重命名或改进任何内容的阶段。)