奇怪的递归宏输出

奇怪的递归宏输出

奇怪的递归宏输出

以下代码描述了一个可以递归迭代(即 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参数55,而最后一次调用的第一个参数\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 打印所有TeXs。事实是,只有一次递归,但最后的递归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,它不知道如何解析。你会得到一个无限循环。

相关内容