我经常发现,当我查看这里专家的解决方案时,宏定义对我来说完全是不可思议的。尝试使用人们用来理解大多数编程语言中的代码的标准技术通常没有帮助;例如,我发现使用追逐定义show
可能会陷入仍然没有意义的东西(对我来说)。
现在,@David Carlisle 刚刚非常友好地教了我一个 TeX 习语,这让很多宏突然变得有意义了。这个问题很大程度上是出于想与我这个级别的其他人分享的愿望。它要求描述任何常见的习语:
- 经常出现在软件包中,并且有来自此处专家的解答
- 正在执行一些仅从命令理解无法明显看出的事情。(例如,
lccode
习惯于与小写字符无关的事情。)
我刚刚学到的成语是回答如下,应该有助于说明我的意思。
答案1
其中之一令人沮丧的功能TeX 宏扩展语言的一个缺点是控制代码和它所控制的代码可以意外地交互。这意味着如果你想让条件块\ifwhatever...\fi
控制某个宏\macro
影响内容的宏后街区,你需要跨越式发展 \macro
首先完成条件:
\ifwhatever
\expandafter\macro
\fi
<stuff taken as an argument by \macro>
你看,\expandafter
吃掉了\fi
之前的\macro
那个。使用这个技巧,你可以编写一些代码,有条件地执行一些后续代码。假设你有一个条件\ifwhatever
,并且你想<code>
在它为真时执行,但在它为假时不执行。那么你可以写(这里我假设\makeatletter
是活动的):
\ifwhatever
\expandafter\@firstofone
\else
\expandafter\@gobble
\fi
{<code>}
如果你想要在两段不同的代码中进行选择,那么这可以概括为顺便一提LaTeX 风格条件语句的\ifwhatever{<true code>}{<false code>}
工作原理如下:
\ifwhatever
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
{<code 1>}
{<code 2>}
不过,我不建议使用多个条件来执行此操作:如果你想<code>
在\ifwhatever
或者 \ifnot
是真的,你必须写:
\ifwhatever
\expandafter\@gobble
\else
\ifnot
\expandafter\expandafter\expandafter\@firstoftwo
\else
\expandafter\expandafter\expandafter\@gobble
\fi
\fi
{<code>}
这种大量的\expandafter
s 是为了跳过\@firstoftwo
或\@gobble
并首先扩展一个标记,然后再扩展另一个标记,然后在没有任何障碍物阻碍时返回扩展您真正想要的宏。
当然,使用多个\expandafter
s 来跳过的技巧本身就是一个习语例如,你可以将组中的第一个标记展开一次:
\expandafter{\macro}
或两次:
\expandafter\expandafter
\expandafter{\macro}
或三次:
\expandafter\expandafter\expandafter\expandafter
\expandafter\expandafter
\expandafter{\macro}
你看到了这个模式。我也可以解释一下为什么会这样。当第一个\expandafter
爆炸时,它会依次膨胀第三个,然后是下一行的第一个,然后是下一行的第一个,最后,\macro
。中间的标记不会在这次“运行”中扩展,而只是按顺序返回。效果是每隔一列s\expandafter
消失,\macro
被扩展一次,然后 TeX 再次开始扩展现在的第一个\expandater
,也就是第二个。配置变成了扩展\macro
两次的配置,所以我们最终将其扩展了三次。
答案2
lccode + 小写技巧
@David Carlisle 的技巧适用于您想要重新定义标准(例如)ASCII 字符(例如-
或 )的行为\n
的情况。这在逐字逐句的环境中很有用,您可能希望以非标准方式处理换行符 - 我将在本帖的其余部分继续使用该示例。
\catcode
TeX 允许你使用和把任何这样的字符变成宏\active
,这让你可以很容易地改变 的行为\n
。问题是,一旦你这样做了,那么你源代码会开始表现不同,这使得实际编写任何内容变得相当困难。因此,您需要某种方式能够在源代码中使用“非活动”换行符,同时将用户输入的任何换行符视为“活动”。
据我了解,这个技巧的工作原理如下:
你可以任意选择一个不需要在宏中使用的活动角色。
~
这是一个不错的选择,因为它始终处于活动状态。如果你需要更多,你可以让任何你喜欢的角色处于活动状态。你“告诉” TeX (比如说)的小写版本
~
是\n
,就像这样:\lccode`~=`\^^M
这当然与实际的小写文本无关。
您将整个宏包装在 中
\lowercase{...}
。这意味着每当您使用 时~
,TeX 都会将其转换为\n
。由于~
处于活动状态,它会将其转换为积极的\n
\n
。这特别意味着您可以像这样重新定义行为:\lowercase{ ... \def~{...} ... }
同时,宏中的正常换行符(例如最后一个换行符之前的换行符
}
)保持非活动状态,因此 TeX 会以正常方式进行处理。
[至少,这是我最理解的……]
答案3
我想我们应该提到
{\ifnum0=`}\fi
\ifnum0=`{\fi}
tabular
与包相关的括号组构造和相关对齐构造中特有的括号组构造。
要理解这些构造,最好从中间开始:反引号构造 `{
返回字符代码{
为 123,特别是它不是 0,所以
\ifnum0=`}\fi
就像\iffalse\fi
并扩展为零。同样
\ifnum0=`{\fi
因此,第一个构造扩展为显式{
,第二个构造扩展为显式}
有一些更简单的结构可以扩展为显式括号
{\iffalse}\fi
\iffalse{\fi}
然而,正如我们将看到的,它们不能以所需的方式在表格结构中工作。
现在考虑一个简单的halign
构造(为了简单起见,仅使用普通的 TeX),它定义居中列,并且(像 LaTeX 一样)希望使用局部定义来\\
结束行。
\def\tabc{%
\leavevmode
\vtop\bgroup\let\\\cr
\halign\bgroup&\quad\hfil\ignorespaces##\unskip\hfil\quad\cr}
\def\endtabc{%
\crcr\egroup
\egroup}
\tabc
1 & 2 & 3\\
aaa & bbb & ccc
\endtabc
\bye
这显然工作正常并且产生
然后有人决定要嵌套表格……
1
用嵌套的 2x2 表替换第一个单元格中的:
\tabc
\tabc a & b \cr x & y \endtabc & 2 & 3\\
aaa & bbb & ccc
\endtabc
失败并显示错误消息
! Missing } inserted.
<inserted text>
}
<to be read again>
\endtemplate
<template> \unskip \hfil \quad \endtemplate
\tabc ->\leavevmode \vtop \bgroup \let \\
\cr \halign \bgroup &\quad \hfil \i...
l.14 \tabc
a & b \cr x & y \endtabc & 2 & 3The hint to understanding the error message is the `\\ ` at the end of the line has cause `\endtemplate` to be read. What has happened is that the intended meaning of locally defining `\\ ` has not happened: as soon as TeX saw the `\\ ` (which is `\let` to `\cr` already because of the outer table, the cell of the outer table ended, the the `\let` then defined `\unskip` to be `\hfil`, the `quad` made a space, but then the cell ended in while the group started by `\halign\bgroup` is still open, so an error was generated.
一个解决方法是将内表嵌套在显式括号中:
\tabc
{\tabc a & b \cr x & y \endtabc} & 2 & 3\\
aaa & bbb & ccc
\endtabc
按预期工作并产生
有时您会看到表宏具有类似的要求,即嵌套实例必须用显式括号保护。但通常情况下,最好使宏在嵌套使用时是安全的。
由于宏定义的解析方式,无法简单地将 添加到{
的定义中\tabc
。另外通常的隐式括号\bgroup
在这里不起作用,请注意 的重新定义\\
已经在由 开始的组内,\bgroup
但 TeX 的表格单元格扫描器无法以正确的方式看到它。事实证明,开头的构造正是所需的。如果我们定义表宏如下所示,则嵌套表将按预期工作,而无需在文档中使用额外的括号:一旦内部\tabc
在外部表的第一个单元格中展开,就会{\ifnum0=`}\fi
展开,然后 TeX 将不会关闭外部表的单元格,直到看到匹配的结束组,即使它看到通常会结束单元格的&
或标记。\cr
\def\tabc{%
{\ifnum0=`}\fi
\leavevmode
\vtop\bgroup\let\\\cr
\halign\bgroup&\quad\hfil\ignorespaces##\unskip\hfil\quad\cr}
\def\endtabc{%
\crcr\egroup
\egroup
\ifnum0=`{\fi}}
\tabc
\tabc a & b \cr x & y \endtabc & 2 & 3\\
aaa & bbb & ccc
\endtabc
\bye
答案4
我列出了用于解决特定问题,而且(至少在我看来)非常当你需要面对问题时很难想出这个成语。
另请参阅评论中的链接问题。
我可能错过了很多,因为现在我知道了这个成语的定义,所以我可以解决这个问题。
你想要什么 | 如何解决 | 包裹在 LaTeX 函数中 |
---|---|---|
将不可扩展函数的结果作为其他函数的参数传递 | 让第一个函数将结果存储在某处 | 不适用 |
未定义的控制序列 | \let \a = \undefined (其中 \undefined 表示未定义) | \cs_undefine:N(全局作用) |
执行一些特殊类别代码(将特殊类别代码的令牌注入令牌列表/代码) | 小写字母技巧 | regex_replace 等等 |
展开第二个参数一次 / expandafter 跳过大量标记 | 参数交换技巧 | \exp_args:Nno |
(与上面类似)将令牌列表的值注入另一个令牌列表深处 | 定义辅助函数/ \edef \noexpand | 无(偶尔可以使用正则表达式。请注意 \tl_replace_all 不会递归) |
比较两个标记列表相等 | 将它们存储到两个宏中然后\ifx |
\tl_if_eq:nnTF |
检查 token 列表是否为空(可扩展) | 基本思想:\if \relax <token list> \relax <true code> \else <false code> \fi (同样去标记化,详情见链接文章 ←) |
\tl_if_empty:n |
迭代标记列表的项目 | 递归宏,逐个获取项目(参见 interface3.pdf 中的“使用 quark 的递归示例”部分) | \tl_map_function:nN |
保留分隔参数的括号 | 为什么 TeX 会删除分隔参数周围的括号? | 没有任何 |
迭代代币代币列表 | (见下文) | (见下文) |
动态创建控制序列,而不必将其定义为 \relax | 将扩展包装在 begingroup...endgroup 中 | 没有任何 |
逐步构建代币列表,允许部分不平衡 | 使用\iffalse { \else } \fi 技巧然后 x 展开完整的标记列表。另请参阅支撑技巧 |
gtl 包(缺点:无法转换回正常 tl,忽略 charcode {} ) |
将给定的字符代码(例如 97)转换为 token | \lowercase 诡计 |
(无包装) |
将给定的字符代码(例如 97)转换为 token可扩展 | \Ucharcat (XeTeX) / tex.cprint (LuaTeX) / 考虑 256 种情况 | \char_generate:nn |
关于代币列表操纵,一般有 3 种情况
你想要什么 | 如何解决 | 包裹在 LaTeX 函数中 |
---|---|---|
1. 可扩展,标记列表作为参数提供 | etl 包裹等 |
|
检查 tl 中的第一个标记是否为显式空间 | 使用 #1 以明确的空格分隔,然后检查 #1 是否为空 | \tl_if_head_is_space:n |
吸收空间 | 定义由显式空格分隔的宏 | 没有任何 |
检查 tl 中的第一个标记是否为括号组 | \string 第一个 token 并检查是否不平衡 | \tl_if_head_is_group:n |
获取 { 或的字符代码} |
获取参数中第一个左括号的\string-ification?/获取参数中第一个左括号的匹配右括号的\string-ification? | |
2. 不可扩展,标记列表作为参数提供 | \tl_analysis_map_inline ,tokcycle 包裹 |
|
获取以下标记的含义 | \futurelet |
peek_after:Nw(低级) |
获取 2-next 标记的含义 | \afterassignment +\futurelet 组合 |
不适用 |
区分具有相同含义的标记,例如显式标记和隐式标记作为活动字符 | 暂时将一些可疑项重新定义为未定义 - 阅读 source3.pdf 上的 \tl_analysis_map_inline 和 \peek_analysis_map_inline 以获取更多详细信息;然后使用\string |
不适用 |
3. 不可扩展,标记列表是输入流中的以下标记 | peek_analysis_map_inline (并非在所有情况下都有效,请阅读来源 3) |
|
4.(额外的)可扩展,标记列表是输入流中的以下标记 | directlua / immediateassignment (仅限 LuaTeX 引擎) |
没有任何 |
有些事情你不知道真的需要(有笨重的解决方法),但如果可以的话会更方便:
你想要什么 | 如何解决 | 包裹在 LaTeX 函数中 |
---|---|---|
比较两个标记列表是否相等(可扩展) | ???? | \etl_if_eq:nnTF(etl 包) |
在一个扩展步骤中完成复杂的工作 | \romannumeral 诡计/ \扩展原语 |
不是的。(在 expl3 中你可以只使用 :e-type 参数或)\exp:w ⟨expandable tokens whose expansion at some stage yields a leading token \exp_end: ⟩ |
在仅扩展上下文中修改值/状态 | \csname (something) \endcsname 如果未定义,则将 (something) 定义为 \relax;\ifcsname 检查 | expl3 中的 l3flag 包(非标准技巧,通常通过传递参数值来处理,就像函数式编程一样) |
使宏可扩展 | 看使宏可扩展的技巧 | 并不真地? |
一旦了解了该功能,您可能就能自己弄清楚这些事情:
你想要什么 | 如何解决 | 包裹在 LaTeX 函数中 |
---|---|---|
在赋值给函数之前先展开参数一次 | \expandafter \函数 \expandafter { \参数 } | \exp_args:否 |
在给出函数之前充分展开参数 | \edef 或 \expanded | \exp_args:Nx |
转换a 为字符码(例如 97) |
\number `a (备注:`a 其本身已经是一个(整数表示),例如"FF 255) |
\int_eval:n |