奇怪的递归宏输出
以下代码描述了一个可以递归迭代(即 for 循环)并使用 e-TeX 的宏numexpr
。它基于在e-TeX 手册 (第 9 页)
代码的第一部分非常简单。在 的第二个定义中foo
,宏似乎循环了两次!
0,1,2,3,4,5
TeX
TeX
TeX
TeX
TeX
这里有些地方看起来不对劲!这让我想起了 JavaScript 中的闭包。你能解释一下这种行为吗?
问题还有第二部分。e-TeX 手册定义了类似的函数:
\def\foo#1#2{\noindent\number#1
\ifnum#1<#2,
\expandafter\foo
\expandafter{\number\numexpr#1+1\expandafter}%
\expandafter{\number#2\expandafter}%
\fi}
这些都\expandafters
用来做什么?它们似乎没有必要。逗号不应该放在\number#1
第一行后面而不是后面吗ifnum
?
以下是包含所有三个示例的代码:
\documentclass[11pt]{article}
\begin{document}
\def\foo#1#2#3{\noindent\number#1, \TeX\par
\ifnum#1<#2
\foo{\number\numexpr#1+\number#3}{\number#2}{\number#3}%
\fi}
\foo{0}{5}{1}
\bigskip
\def\foo#1#2#3{\noindent\number#1,
\ifnum#1<#2
\foo{\number\numexpr#1+\number#3}{\number#2}{\number#3}%
%\else\relax
\noindent\TeX\par
\fi}
\foo{12}{18}{1}
\bigskip
\def\foo#1#2{\number#1, \TeX\par
\ifnum#1<#2
\expandafter\foo
\expandafter{\number\numexpr#1+1\expandafter}%
\expandafter{\number#2\expandafter}%
\fi}
\foo{12}{18}
\end{document}
答案1
现在,我仅回答您的第二个问题,即有关 e-TeX 中的示例。它们\expandafter
用于强制 TeX 在\foo
递归调用之前实际进行计算,并清除结尾\fi
以使递归终止。这不是绝对必要的,但是一种优化。以下是直观检查我的第一个断言的方法:
\documentclass{minimal}
\def\foo#1#2{\noindent\number#1
\ifnum#1<#2,
\expandafter\foo
\expandafter{\number\numexpr#1+1\expandafter}%
\expandafter{\number#2\expandafter}%
\fi}
\def\noexpfoo#1#2{\noindent\number#1
\ifnum#1<#2,
\noexpfoo
{\number\numexpr#1+1}%
{\number#2}%
\fi}
\begin{document}
\tracingmacros1
\foo{1}{5}
\noexpfoo{1}{5}
\tracingmacros0
\end{document}
编译完此文件后,查看日志。您可以检查最后一次调用的\foo
参数5
和5
,而最后一次调用的第一个参数\noexpfoo
是\number \numexpr \number \numexpr \number \numexpr \number \numexpr 1+1+1+1+1
,它本质上是 5 以一为基数的表示,而不是本例中使用的十为基数的表示\expandafter
。在无 expandafter 的情况下,第一个参数的大小呈指数增长。这非常糟糕,会导致 TeX 因内存不足而崩溃,或者至少在大循环中运行非常缓慢。
清除 final 的\fi
作用与使宏更具可扩展性相同,只是方式不同。如果\expandafter
的定义中没有 final \foo
,则在 final 调用时,TeX 的输入堆栈将看起来像\foo{5}{5}\fi\fi\fi\fi\fi
,其中包含与宏调用相关的所有信息,不同的信息\fi
都来自此。如果使用 final \expandafter
(由第一个间接触发),则输入堆栈看起来像\foo{5}{5}
没有任何尾随的\if
。
我没有包含有关\expandafter
链接和条件扩展方式的更多细节以保持答案的集中,但如果需要,请随时询问。
另外,逗号位于\ifnum
测试之后,因此您1, 2, 3, 4, 5
无需像 那样在后面加上逗号空格1, 2, 3, 4, 5,
。
补充关于扩展。因此,在 的定义中\foo
有 5\expandafter
和 3 \number
。第一个\expandafter
扩展 之后的所有内容\foo
,而 恰好是第二个\expandafter
,它又扩展了括号后面的内容,即第二个\number
(第一个在宏中靠前)。现在,\number
尝试读取一个数字,扩展所有内容,直到找到不属于数字的内容。这里,右括号将停止该过程,但直到第三个\expandafter
扩展后才会被命中,这会触发第四个\expandafter
,这又会触发最后一个\number
,在处理到右括号为止的所有内容的过程中,会触发下一个\expandafter
,它会扩展括号后面的所有内容,也就是... 等一下... \fi
。
现在,关于 TeX 如何处理(扩展)条件的一个常见误解是忘记了\fi
仍然存在。当 TeX 扩展 时\ifnum
,它没有读取直到 的所有内容\fi
。这就是宏对其参数所做的:完全读取它们。但条件分支不是宏参数。相反,当扩展\ifnum
并发现它是 true 时,TeX 会继续读取其余部分,并提醒自己,下一个\fi
遇到的是完全正常的,应该默默地扩展为空。如果它遇到\else
第一个,那么它将忽略它以及直到(并包括)结束的所有内容\fi
。所以,仅有的被忽略的分支将被视为一个块。
答案2
现在,关于你的第一个问题。没有进行双重递归。你似乎相信宏 recurse-loops 打印1, 2, 3, 4, 5
并再次 recurse-loops 打印所有TeX
s。事实是,只有一次递归,但最后的递归TeX
被保存在一个堆上并在递归结束时弹出。也就是说,当最后一次调用时\foo
,TeX 的输入流看起来像\foo{arg1]{arg2}{arg3}\noindent\TeX\par\noindent\TeX\par\ETC.
下面是一个示例,通过在每次调用时改变输出颜色使其更加直观\fooX
。
\documentclass{article}
\usepackage{xcolor}
\newcommand\nbcolor[2]{%
\color{green!\number\numexpr(#2-#1)*100/#2\relax!blue}}
\def\fooa#1#2#3{
\begingroup \nbcolor{#1}{#2}%
\noindent\number#1, \TeX\par
\ifnum#1<#2
\fooa{\number\numexpr#1+\number#3}{\number#2}{\number#3}%
\fi \endgroup}
\def\foob#1#2#3{%
\begingroup \nbcolor{#1}{#2}%
\noindent\number#1,
\ifnum#1<#2
\foob{\number\numexpr#1+\number#3}{\number#2}{\number#3}%
\noindent\TeX\par
\fi \endgroup}
\def\fooc#1#2#3{
\begingroup \nbcolor{#1}{#2}%
\noindent\number#1,
\ifnum#1<#2
\fooc{\number\numexpr#1+\number#3}{\number#2}{\number#3}%
\else\relax
\noindent\TeX\par
\fi \endgroup}
\begin{document}
\fooa{0}{5}{1} \bigskip
\foob{0}{5}{1} \bigskip
\fooc{0}{5}{1} \bigskip
\end{document}
我希望它能更清楚地说明\foob
只循环一次。最后一次\TeX
调用是第一次调用\foob
,倒数第二次调用是第二次调用 ,等等。
顺便说一句,这与 TeX 无关,而是递归的普遍事实:根据递归调用和其余代码的相对位置,您不会获得相同的结果。
答案3
不确定你想要发生什么,但如果你将第二个例子改为
\def\foo#1#2#3{\noindent\number#1,\TeX\par
\ifnum#1<#2
\foo{\number\numexpr#1+\number#3}{\number#2}{\number#3}%
\fi}
\foo{12}{18}{1}
你会得到
12、TeX
13、TeX
14、TeX
15、TeX
16、TeX
17、TeX
18、TeX
对于最后一个例子,你可以自己判断 s 是否\expandafter
必要,方法是将其取出并观察会发生什么。本质上,\ifnum
不会根据数字进行求值,而是根据 进行求值\number\numexpr#1+1
,它不知道如何解析。你会得到一个无限循环。