如何解开 edef 语句中的明显递归?

如何解开 edef 语句中的明显递归?

我正在编写的教科书需要能够动态构建按字母顺序排序的列表。为了不调用高性能通用包的开销,我选择自己编写一个,这既有趣又有启发性。

任何字母排序例程都必须使用排序键来确定当前短语在列表中的排序位置,方法是将其排序键与列表中其他每个短语的键进行比较。该例程实现了所需的排序规则,例如忽略大小写、数字、空格和标点符号,以及删除任何嵌入的格式化命令,包括字体和字体系列更改和重音(变音)标记(删除重音,保留字符)。此例程不仅会删除字符(空格和标点符号等),还可以直接更改字符本身,例如更改其大小写。完成所有这些任务的一种方法是逐个字符地浏览要排序的短语,将这些规则分别应用于每个字符,最后通过将结果字符附加到已处理的字符串来构建键。这最后一步,将字符附加到字符串,是这个问题的基础。

我已经使用(全局)语句实现了此附加过程,如下面的 MWE 中的宏\edef所示:\appendChar

\documentclass{article}

\usepackage[svgnames]{xcolor}% to get named colors

\def\RLp{{\color{Red}\tt(}} \def\RRp{{\color{Red}\tt)}}% (input)
\def\RLb{{\color{Red}\tt[}} \def\RRb{{\color{Red}\tt]}}% [output]

\gdef\ttt{\tt\char92}% A better \tt \textbackslash

\def\appendChar#1{% Append argument character to string
  \global\edef\resultStr{\resultStr#1}%
  \par{\tt~\RLp#1\RRp~~~\RLb\resultStr\RRb}\par%
}

\def\testAppendOne#1{% Use counter and \chardef; FAILS
  \newcount\chNum \chNum=\numexpr(`#1)%
% Do arithmetic on the counter to implement the sorting rules
  \chardef\myCh=\chNum%
  \appendChar{\char\myCh}%
}

\def\testAppendTwo#1{% Use counter, \chardef and \the ; WORKS
  \newcount\chNum \chNum=\numexpr(`#1)%
% Do arithmetic on the counter to implement the sorting rules
  \chardef\myCh=\chNum%
  \appendChar{\char\the\myCh}%
}

\def\testAppendThree#1{% Bypass \chardef, WORKS
  \newcount\chNum \chNum=\numexpr(`#1)%
% Do arithmetic on the counter to implement the sorting rules
  \appendChar{\char\the\chNum}%
}

\def\useMacro#1{% Calls macro #1{<character>}
  \global\edef\resultStr{}% (Re)initialize to empty string:
  \medskip
  \par{\tt\RLp chr\RRp~~\RLb\ttt resultStr\RRb}\par
  #1{a}% Simulate character data from string parsing macro
  #1{b}%
  #1{c}%
  #1{d}%
  #1{e}%
  \medskip
}

\begin{document} %               B E G I N   D O C U M E N T

% Proof of principle of using \edef to rebuild a string
\noindent{\Large\bf\color{Blue}Using {\ttt resultStrAppend}:}
\useMacro{\appendChar}

% First attempt fails with apparent recursion
\bigskip\noindent{\Large\bf\color{Blue}Using {\ttt testAppendOne}:}
\useMacro{\testAppendOne}

% Second attempt: adding \the command eliminates recursion
\bigskip\noindent{\Large\bf\color{Blue}Using {\ttt testAppendTwo}:}
\useMacro{\testAppendTwo}

% Third (final) macro: don't need the \chardef, have working macro
\bigskip\noindent{\Large\bf\color{Blue}Using {\ttt testAppendThree}:}
\useMacro{\testAppendThree}

\end{document} %                 E N D   D O C U M E N T

我们用\appendChar四种不同的方式调用宏:

  1. 使用\appendChar首先,我们\appendChar直接调用来提供“原理证明”,即使用\edef语句来(重新)构造一个字符串,乍一看似乎是递归,但显然不是如屏幕截图所示。
  2. 使用\testAppendOne\chNum这将使用计数器和语句处理输入字符,\chardef然后再将其传递给\appendChar。生成的字符串\resultStr是完全错误的。在n第个字符,全部n中的字符\resultStr已经变成了刚刚输入的字符!这当然看起来就像递归,但真的是这样吗?
  3. 使用\testAppendTwo此宏与 完全相同,只是在 调用 时\testAppendOne添加了命令。我们发现这会停止“递归”并生成正确的结果。\the\appendChar
  4. 使用\testAppendThree经过一些实验,发现不需要\chardef。这是最终的、有效的宏。

上面的第 2 项是这个问题的核心。尽管我对附加过程有一个可行的解决方案,但宏的奇怪行为\testAppendTwo引起了我的注意,我试图了解发生了什么。我没有成功。因此,我的问题是:

宏中到底发生了什么\testAppendOne?我感兴趣的不仅仅是对什么是正在发生的事情,而且还请描述我应该采取什么步骤来自己解决这种行为。

我怀疑这可能包括使用一些\tracing...宏,但我还没有找到可用的描述如何使用这些宏,与什么他们确实这么做了,但即便如此,这也只是很粗略的。

测试输出

答案1

首先对您的代码进行一些一般性评论:

在 LaTeX 中,使用\bfseries代替\bf\ttfamily代替\tt。自 20 多年前 LaTeX2ε 发布以来,2 个字母的字体更改命令已经过时。

\global\edef可以缩写为\xdef。但是,宏不会创建组级别,因此由于您在没有任何分组层的情况下使用了这一切,因此\global这里的分配是不必要的(当然,除非您在实际代码中需要它,那么请忽略这一点)。

打印命令名称的一个技巧是,您\ttt command可以使用 来实现\ttfamily\string\command,或者,如果您定义了\def\ttt{\ttfamily\string},那么\ttt\command

有一点非常重要:不要多次分配寄存器。这样只会耗尽可用寄存器的数量。分配一次寄存器,然后根据需要多次使用它们。我移出了\newcount\chNum宏定义。

在数字赋值中,TeX 已经理解了\chNum=`#1如何获取字母常量的数值;在这里你不需要(但也不会有什么坏处) 。此外,这里也不需要周围\numexpr的额外一对括号。`#1\numexpr

现在开始\testAppendOne。代码

\chNum=\numexpr(`#1)%
\chardef\myCh=\chNum%
\appendChar{\char\myCh}%

将两个标记附加\char\myCh\resultStr。但是,由于\char基元不可扩展, 的内容\resultStr\char\myCh第一次迭代之后、\char\myCh\char\myCh第二次迭代之后,依此类推。当您要求 TeX 写入 的内容时,它将\resultStr使用\char\myCh\char\myCh...并将\myCh最后一个值分配给它,这将是您处理的最后一个字母,因此您会看到这样的行为。

当你处理 时a \myCh\char"61也会\char\char"61a。很明显。当你处理b \myCh\char"62\char\char"62\char\char"62也会是 ,bb依此类推。要解决这个问题,你必须给出\appendChar一些即使 改变也不会改变的东西\myCh。代码

\chNum=\numexpr(`#1)%
\chardef\myCh=\chNum%
\appendChar{\char\myCh}%

是多余的。在将其传递给 之前,您不需要使用\chardef字符\char。正如您自己使用 完成反斜杠一样\char92,您可以在此处使用数字:

\chNum=`#1
\appendChar{\char\chNum}%

甚至:

\appendChar{\char`#1}%

这将会传递\char`<some number which will not change>下去\appendChar,你就会得到你想要的。

但为什么\char\the\myCh中的\testAppendTwo有效?因为原语\the将访问 中的值\myCh。假设\myCh\char"61( a),那么\the\myCh将是"61(十六进制) 或97。因此代码

\chNum=\numexpr(`#1)%
\chardef\myCh=\chNum
\showthe\myCh
\appendChar{\char\the\myCh}%

也是多余的,因为\the\myCH将访问中的数字\myCh,即\chNum,因此可以简化为:

\chNum=\numexpr(`#1)%
\appendChar{\char\chNum}%

\testAppendThree是一样的,只不过\the\chNum会获取寄存器中的值\count而不是\char。但它们是相同的,因此结果是相同的(并且与 : 的修改版本相同\testAppendTwo)。


现在进入第二部分,如何调试 TeX 代码... 好吧... 你其实并不想进入这里。如果你不进入这里,你会更开心(只是开玩笑... 还是我开玩笑?)。

您的代码相当简单,因此我设法仅使用\show和来调试它\showthe。它们都用作\show<macro>\showthe<register>

\show将宏的内容打印到控制台。例如,如果我将你的\appendChar宏更改为:

\def\appendChar#1{% Append argument character to string
  \xdef\resultStr{\resultStr#1}%
  \show\resultStr
  \par{\ttfamily~\RLp#1\RRp~~~\RLb\resultStr\RRb}\par%
}

运行\testAppendOneTeX 将在终端中显示以下内容:

> \resultStr=macro:
->\char \myCh .
\appendChar #1->\xdef \resultStr {\resultStr #1}\show \resultStr 
                                                                 \par {\ttfamily ~\RLp #1\RRp ~~~\RLb \resultStr \RRb }\par 
l.63 \useMacro{\testAppendOne}

? 

然后,如果我按<return>,它会显示:

> \resultStr=macro:
->\char \myCh \char \myCh .
\appendChar #1->\xdef \resultStr {\resultStr #1}\show \resultStr 
                                                                 \par {\ttfamily ~\RLp #1\RRp ~~~\RLb \resultStr \RRb }\par 
l.63 \useMacro{\testAppendOne}

? 

(注意第二行)。这向我展示了为什么你\testAppendOne像我之前描述的那样多次打印同一封信。

\showthe类似,但它显示的是寄存器的值。例如,如果你想查看寄存器的值,\chNum可以使用\showthe\chNum

这两个宏具有等效项,它们不是打印到控制台(和日志),而是打印到 PDF。\meaning<macro>将打印的定义<macro>\the<register>打印其值。


调试不够?

你可以使用\tracingall宏,它告诉 TeX“给我你要展示的所有内容”(然后\tracingnone告诉它何时停止),然后你的控制台将充满文本。通过一些练习,你将学会如何阅读它。

您还可以加载trace包并使用\traceon/ \traceoff,这将隐藏一些打印相当长的跟踪文本的代码步骤,例如 LaTeX 的字体选择系统。


还不够吗?

加载\usepackage{unravel}(毕竟你要求解开 :) 并使用\unravel{<code which is keeping you awake at night>},包将逐步向您展示 TeX 正在做什么。当你被一些令人讨厌的代码卡住时,它非常有用。请注意,这个分步说明是真的彻底,所以可能需要几个步骤\unravel{\useMacro{\appendChar}}:)

答案2

您正在使用

  \chardef\myCh=\chNum%
  \appendChar{\char\myCh}%

既不是\char 也不是\chardef定义的标记,例如\myCh是不可扩展的,所以你的重复\edef只会产生

\char\myCh\char\myCh\char\myCh\char\myCh

当你最终使用宏时,你只会获得最后定义的多个副本\myCh

答案3

您可以通过添加类似以下内容进行调试

\texttt{\meaning\resultStr}

输出。如果你在原始代码上执行此操作,你将获得,对于\testAppendOne

在此处输入图片描述

\testAppendTwo\testAppendThree得到

在此处输入图片描述

这也不是你想要的,不是吗?

这是一个修复版本,\testAppendOne它用一个技巧附加了真实字符\lowercase,即

\def\testAppendOne#1{% Use a better approach; WORKS
  \chNum=\numexpr(`#1)\relax
% Do arithmetic on the counter to implement the sorting rules
  \begingroup\lccode`!=\chNum\lowercase{\endgroup\appendChar{!}}%
}

注意其他修复:\ttfamily使用 代替\tt,类似地\bfseries用于\bf;更重要的是,您不应该\newcount\chNum在每次调用时都使用:您会在每次宏调用时浪费一个计数寄存器。我还简化了初始宏。

我为什么要使用\lccode`!=\chNum?该字符!的类别代码为 12,因此我们基本上将所有类别代码标准化为 12,这在处理字符串时似乎是合适的。

\documentclass{article}

\usepackage[svgnames]{xcolor}% to get named colors

\newcommand{\Rtt}[1]{\textcolor{Red}{\ttfamily#1}}
\newcommand\RLp{\Rtt{(}}\newcommand\RRp{\Rtt{)}} % input
\newcommand\RLb{\Rtt{[}}\newcommand\RRb{\Rtt{]}} % [output]

\newcommand\ttt{{\ttfamily\char92}} % A better \tt \textbackslash

\newcount\chNum % NOT inside macros

\def\appendChar#1{% Append argument character to string
  \global\edef\resultStr{\resultStr#1}%
  \par{\ttfamily~\RLp#1\RRp~~~\RLb\resultStr\RRb}\par
}

\def\testAppendOne#1{% Use a better approach; WORKS
  \chNum=\numexpr(`#1)\relax
% Do arithmetic on the counter to implement the sorting rules
  \begingroup\lccode`!=\chNum\lowercase{\endgroup\appendChar{!}}%
}

\def\testAppendTwo#1{% Use counter, \chardef and \the; DOESN'T REALLY WORK
  \chNum=\numexpr(`#1)\relax
% Do arithmetic on the counter to implement the sorting rules
  \chardef\myCh=\chNum
  \appendChar{\char\the\myCh}%
}

\def\testAppendThree#1{% Bypass \chardef, DOESN'T REALLY WORK
  \chNum=\numexpr(`#1)\relax
% Do arithmetic on the counter to implement the sorting rules
  \appendChar{\char\the\chNum}%
}

\def\useMacro#1{% Calls macro #1{<character>}
  \global\edef\resultStr{}% (Re)initialize to empty string:
  \medskip
  \par{\tt\RLp chr\RRp~~\RLb\ttt resultStr\RRb}\par
  #1{a}% Simulate character data from string parsing macro
  #1{b}%
  #1{c}%
  #1{d}%
  #1{e}%
  \medskip
  \texttt{\meaning\resultStr}% for debugging
  \medskip
}

\begin{document} %               B E G I N   D O C U M E N T

% Proof of principle of using \edef to rebuild a string
\noindent{\Large\bfseries\color{Blue}Using {\ttt resultStrAppend}:}
\useMacro{\appendChar}

% First attempt fails with apparent recursion
\bigskip\noindent{\Large\bfseries\color{Blue}Using {\ttt testAppendOne}:}
\useMacro{\testAppendOne}

% Second attempt: adding \the command eliminates recursion
\bigskip\noindent{\Large\bfseries\color{Blue}Using {\ttt testAppendTwo}:}
\useMacro{\testAppendTwo}

% Third (final) macro: don't need the \chardef, have working macro
\bigskip\noindent{\Large\bfseries\color{Blue}Using {\ttt testAppendThree}:}
\useMacro{\testAppendThree}

\end{document}

在此处输入图片描述

相关内容