![为什么 TeX 会删除分隔参数周围的括号?](https://linux22.com/image/244315/%E4%B8%BA%E4%BB%80%E4%B9%88%20TeX%20%E4%BC%9A%E5%88%A0%E9%99%A4%E5%88%86%E9%9A%94%E5%8F%82%E6%95%B0%E5%91%A8%E5%9B%B4%E7%9A%84%E6%8B%AC%E5%8F%B7%EF%BC%9F.png)
在 TeXbook 的第 203-204 页,Knuth 描述了 TeX 如何吸收宏的参数:带分隔符的参数会一直运行到分隔符第一次出现的位置,而无分隔符的参数会一直运行到下一个标记,这两种情况都尊重括号组。但随后他又说道:
在这两种情况下,如果以这种方式找到的参数具有以下形式
'{<nested tokens>}'
,其中<nested tokens>
代表任何根据括号正确嵌套的标记序列,则将删除包围参数的最外层括号,而保留<nested tokens>
。
对于分隔参数来说,这是一个非常烦人的功能!例如,在这个问题,没有好的方法来处理可能是一个单个括号参数或一对参数的宏\macro#1\pgfeov
,#1
因为括号会被参数扫描器删除,然后结果(如果不是双括号)看起来像多个标记,因此被(内部宏)捕获为多个参数,即使我将其括在括号中。
我查阅了 TeXbook,希望能得到某种解释,但甚至没有关于这个主题的练习。有人能解释一下为什么这是一个有用的功能吗?
编辑:我想澄清一下,我并不是在谈论命令式语言意义上的“参数”,在命令式语言中,人们试图将宏视为函数,将其输入传递为,比如说。\macro[#1]<#2>(#3,#4)
我指的是上面描述的意义:TeX 通过解析文本吸收的标记列表。
我还想补充一点,我对解决方法的兴趣远不及标题中问题的答案:为什么(从哲学上来说)TeX 会这样工作?例如,我知道控制字后的空格被忽略的原因是,如果不这样做,就没有办法忽略一个字母后面加一个空格,因为扫描仪会继续往宏名里添加字母,直到不能再添加为止。所以这条规则有一个好处,它让语言更强大。
答案1
需要注意的是 TeX 的仅有的嵌套参数的方法是括号。您可以定义一个宏,\def\whatever[#1]{...}
但是当您将其调用为 时\whatever[oh[well]]
,事情就会变得很糟糕。但是,将其调用为\whatever[{oh[well]}]
却很顺利,并且\whatever
永远不会注意到它被通过将 插入其参数中而蒙骗了]
。因此,括号可以用作隐藏 TeX 中结束分隔符的手段,而不会真正影响预期的参数。
这也意味着,每当您使用带有非文字参数的分隔参数调用宏时(更确切地说,参数不完全由您自己控制,当您编写宏包供其他人使用时,这种情况经常发生),您应始终在每个分隔参数周围添加一层括号,例如\whatever[{#1}]
或类似。没有其他方法可以确保参数不会被分割成不同的东西,因为它们本身可能包含一个右括号。
答案2
我在这里添加第二个答案,因为现在我正在回答一个不同的隐含问题:如何忠实地再现可能以括号括起来的形式出现的参数\macro#1\pgfeov
?比如把它作为参数\message
?好吧,确保它不是完全用括号括起来的。
\def\macro{\macroii \empty}
\def\macroii#1\pgfeov{\message{#1}}
在不可扩展的上下文中,您可以使用其他易于删除的内容,可能是空参数{}
。如果您打算将其作为分隔参数传递,则可以使用被调用的命令而不是\empty
。
答案3
虽然我很欣赏 David 的回答,但他们似乎没有解决我提出的设计问题。因此,我将尝试利用从我们的讨论中获得的理解来自己回答这个问题。
让我们从分隔参数的用途开始确切地它们看起来的作用是:抓取参数,但忽略某些周围和中间的文本。目标是实际执行文本处理还是仅仅使用不寻常的括号约定并不重要。我声称:
需要有一种机制来防止参数扫描器找到某些原本构成分隔参数的表达式;
这种机制将受影响的文本包裹在括号中,这是合乎逻辑的;
删除这些括号与无限制参数是一致的;
此外最簡單这样做,因为击败第 3 点中的动作所需的解决方法比不移除括号的情况下产生该动作所需的解决方法更容易。
关于第 1 点:当然需要这样的机制,否则可能作为分隔符出现的文本就永远不会出现在参数中(我知道 Knuth 对这种事情感到恼火,因为他反对 TeXbook 附录 D 中的某些逐字打印技术)。
关于第 2 点:正如 David 在他的第一个回答中所说,TeX 中没有其他既定的机制来对事物进行分组(括号是分组字符)。此外,这也是我们如何对未限定参数进行分组的问题,所以这是一个一致性的问题。
关于第 3 点:这里的关键点是,未限定的参数实际上并没有被处理任何与带分隔符的参数不同!描述让带分隔符的参数听起来很特别,但实际上,在问题中,我以一种完全中立的方式总结了参数抓取的行为:抓取参数直到下一个分隔符,然后将其删除。未分隔的参数有空分隔符,仅此而已。
关于第 4 点:好吧,仅仅有逻辑上的理由去做某件事,并不是做这件事的实际理由,而我的问题描述了不是这样做。如果有一个合乎逻辑的理由不这样做,将会有所帮助。因此假设不是从分隔参数中删除;那么第 1 点会迫使您偶尔添加它们,并且必须删除它们。您将如何做?有三种情况需要处理(#1
= 参数):
(a): #1 = abc...
(b): #1 = {abc...}
(c): #1 = {abc}{...}
请记住,abc...
可以包含嵌套组,因此您的解决方案必须尊重嵌套。因此,您要么必须在 TeX 中实现括号解析器,要么必须使用尊重嵌套的一些原语。为了使解决方案可扩展,您必须使用\def
或\edef
。因此:您需要\def
编写一个宏,该宏能够从 (b) 中剥离外部括号,但不从 (c) 中剥离括号,并且不会改变 (a)。这是一个使用 LaTeX 中的一些简单实用宏来处理 (b) 和 (c),但不处理 (a) 的宏:
\def\stripbraces#1#2\@nil{%
% #1 is undelimited, so has its braces removed. If there is no #2, that is good.
\ifx\relax#2%
\expandafter\@firstoftwo
% #2 never has its braces removed, even if it's a single group.
\else
\expandafter\@secondoftwo
\fi
{#1}{{#1}#2}%
}
你使用它作为\stripbraces...\@nil
,它会从 中删除外部括号...
。这是在假设分隔参数没有删除括号的情况下进行的。唉,如果文本有两个或更多标记,但没有括号开头,那么它的第一个标记获取支撑。
现在,可以使用\futurelet
(ie \@ifnextchar
) 来确定是否#1
以括号开头,但这是不可扩展的。这是一个可扩展的版本,灵感来自我在这个答案,总结于这个:
\def\findbrace#1#{%
% The brace is immediately after the macro
\ifx\relax#1%
\expandafter\@firstoftwo
% The next token is not a brace
\else
\expandafter\@secondoftwo
\fi
{\stripbraces}{\remove@nil#1}%
}
\def\remove@nil#1\@nil{#1\@gobble}
当然,使用方式为,并且您还\findbrace...\@nil{}
必须修改放在后面的虚括号。这两个宏一起将捕获 (a)、(b) 和 (c),正如它们应该的那样。\stripbraces
\@gobble
\@nil
此解决方法的替代方法是 David 的第二个答案中描述的:只需\empty
在分隔文本前面添加,然后它就不会被括号包围,但会具有相同的扩展。至少可以说,这要简单得多。
我认为,逻辑一致性和避免极端棘手的结合是采用 TeX 在现实世界中遵循的路线的一个很好的理由,尽管我想观察到,因为我的解决方法确实存在,替代方案并非不可接受(尽管在有人弄清楚之前可能无法实施)。而且 TeX 本身并不完全反对棘手问题,但按照这种逻辑,David 的解决方法目前的情况也很好。所以我没有这么说。
答案4
该tokcycle
包可以提前扫描标记并保存它们。这里,\macro
用分隔符 定义\pgfeov
。宏\macroaux
是将原始参数提供为#1
,括号完整。这里,我只是定义\macroaux
以对传递的内容进行反标记。
\documentclass{article}
\usepackage{tokcycle}[2021/03/10]
\usepackage[T1]{fontenc}
\xtokcycleenvironment\macro
{\whennotprocessingparameter##1{\addcytoks{##1}}}
{\processtoks{##1}}
{\addcytoks{##1}}
{\addcytoks{##1}}
{\let\pgfeov\endtokcycraw}
{\expandafter\cytoks\expandafter{\expandafter\cytoks\expandafter{\the\cytoks}}
\tcafterenv{\expandafter\macroaux\expandafter{\the\cytoks}}}
\newcommand\macroaux[1]{\detokenize{#1}}
\begin{document}
\macro A\pgfeov
\macro{A}\pgfeov
\macro{x{A}yz}x\pgfeov
\macro q{x{{{A}}}yz}x{B}\pgfeov
\newcommand\Q[1]{\macro a{#1}c\pgfeov}
\Q b
\renewcommand\macroaux[1]{#1\Q x}
\macro\def\Q#1{[#1]}\pgfeov
\Q y
\end{document}