在玩字符串的过程中,我发现了三种不同的宏,可以逐个字符地循环遍历字符串。但是,我不太清楚它们的具体工作原理。有人能解释一下它们各自的机制吗?哪一个是最“正确”的?还有其他方法可以构造这个宏吗?
我已经改变了原来的版本,以便它们看起来尽可能的相似。
基于 Tarass 的宏:
对我来说,这是最容易理解的。由于参数是分隔的,因此第一个参数是第一个字母,其余的参数将重新引入循环中。循环在语句内进行\if
。
基于 David Carlisle 的宏:
这个比较棘手。循环来自\if
语句内部,但实际上并非如此。如果\expandafter
消除了,它就不起作用。如果\xloop
将放在后面,\fi
它也不会起作用。那么究竟发生了什么?
其余的部分就简单多了,每次\xloop
执行时,它都会找到字符串的剩余部分并取第一个字符。
基于 Florent 的宏:
这个还是比较复杂的(至少对我来说)。循环在语句外部进行\if
,但字符像以前一样逐个访问。令人惊讶的是,这里没有丢弃空格。这也可以在前面的宏中使用来实现\obeyspaces
。
\documentclass{article}
\begin{document}
\subsection*{Macro based on one by Tarass:}
\def\xloop<#1#2>{%
\ifx\relax#1
\else
(#1)\xloop<#2>%
\fi}
\def\markletters#1{\xloop<#1\relax>}
\markletters{Hello World!}
\subsection*{Macro based on one by David Carlisle:}
\def\xloop#1{%
\ifx\relax#1
\else
(#1)\expandafter\xloop%
\fi}
\def\markletters#1{\xloop#1\relax}%
\markletters{Hello World!}
\subsection*{Macro based on one by Florent:}
\def\gobblechar{\let\xchar= }
\def\assignthencheck{\afterassignment\xloop\gobblechar}
\def\xloop{%
\ifx\relax\xchar
\let\next=\relax
\else
(\xchar)\let\next=\assignthencheck
\fi
\next}
\def\markletters#1{\assignthencheck#1\relax}
\markletters{Hello World!}
\end{document}
答案1
如果我稍微修改一下你的测试,让追踪论证更简短
\documentclass{article}
\def\test#1{{
\tracingonline=1
\tracingmacros=1
\markletters{#1}
}
\typeout{TYPEOUT: \markletters{#1}}
}
\begin{document}
\subsection*{Macro based on one by Tarass:}
\def\xloop<#1#2>{%
\ifx\relax#1
\else
(#1)\xloop<#2>%
\fi}
\def\markletters#1{\xloop<#1\relax>}
\test{a bc}
\subsection*{Macro based on one by David Carlisle:}
\def\xloop#1{%
\ifx\relax#1
\else
(#1)\expandafter\xloop%
\fi}
\def\markletters#1{\xloop#1\relax}%
\test{a bc}
\subsection*{Macro based on one by Florent:}
\def\gobblechar{\let\xchar= }
\def\assignthencheck{\afterassignment\xloop\gobblechar}
\def\xloop{%
\ifx\relax\xchar
\let\next=\relax
\else
(\xchar)\let\next=\assignthencheck
\fi
\next}
\def\markletters#1{\assignthencheck#1\relax}
\test{a bc}
\end{document}
那么第一个测试产生
\markletters #1->\xloop <#1\relax >
#1<-a bc
\xloop <#1#2>->\ifx \relax #1 \else (#1)\xloop <#2>\fi
#1<-a
#2<- bc\relax
\@nobreakfalse ->\global \let \if@nobreak \iffalse
\xloop <#1#2>->\ifx \relax #1 \else (#1)\xloop <#2>\fi
#1<-b
#2<-c\relax
\xloop <#1#2>->\ifx \relax #1 \else (#1)\xloop <#2>\fi
#1<-c
#2<-\relax
\xloop <#1#2>->\ifx \relax #1 \else (#1)\xloop <#2>\fi
#1<-\relax
#2<-
TYPEOUT: (a)(b)(c)
在这里您可以看到宏使用了一个分隔参数,因此每次都会抓取整个列表(所有标记直到>
),并且处理第一个标记,其余的标记会在递归调用中重新插入。
- 因为
#1
它像普通的非分隔参数一样工作,所以它总是会删除空格。 - 由于整个事情都是通过扩展来实现的,因此它只在扩展环境中起作用,
\write
例如TYPEOUT: (a)(b)(c)
\fi
在每个阶段循环后都会插入一个,因此如果您有 1000 个条目,那么就会有 1000 个这样的条目,并且在某些时候您将过度填充输入堆栈。
第二个区块产生
\markletters #1->\xloop #1\relax
#1<-a bc
\xloop #1->\ifx \relax #1 \else (#1)\expandafter \xloop \fi
#1<-a
\@nobreakfalse ->\global \let \if@nobreak \iffalse
\xloop #1->\ifx \relax #1 \else (#1)\expandafter \xloop \fi
#1<-b
\xloop #1->\ifx \relax #1 \else (#1)\expandafter \xloop \fi
#1<-c
\xloop #1->\ifx \relax #1 \else (#1)\expandafter \xloop \fi
#1<-\relax
TYPEOUT: (a)(b)(c)
在这里你可以看到,在第一个宏之后,内部宏不会抓取整个列表,而只是抓取第一个标记。这样做可以避免重新解析列表,并使输入堆栈过载,但你需要在\fi
执行递归调用之前扩展(为零),因为你无法像\fi
第一个版本那样将放在列表之后。因此,\expandafter
这迫使\fi
在之前扩展\xloop
。
第三个版本
\markletters #1->\assignthencheck #1\relax
#1<-a bc
\assignthencheck ->\afterassignment \xloop \gobblechar
\gobblechar ->\let \xchar =
\xloop ->\ifx \relax \xchar \let \next =\relax \else (\xchar )\let \next =\assi
gnthencheck \fi \next
\@nobreakfalse ->\global \let \if@nobreak \iffalse
\next ->\afterassignment \xloop \gobblechar
\gobblechar ->\let \xchar =
\xloop ->\ifx \relax \xchar \let \next =\relax \else (\xchar )\let \next =\assi
gnthencheck \fi \next
\next ->\afterassignment \xloop \gobblechar
\gobblechar ->\let \xchar =
\xloop ->\ifx \relax \xchar \let \next =\relax \else (\xchar )\let \next =\assi
gnthencheck \fi \next
\next ->\afterassignment \xloop \gobblechar
\gobblechar ->\let \xchar =
\xloop ->\ifx \relax \xchar \let \next =\relax \else (\xchar )\let \next =\assi
gnthencheck \fi \next
\next ->\afterassignment \xloop \gobblechar
\gobblechar ->\let \xchar =
\xloop ->\ifx \relax \xchar \let \next =\relax \else (\xchar )\let \next =\assi
gnthencheck \fi \next
! Undefined control sequence.
在这里,该项目是通过\let
分配来抓取的,这具有看到空间标记的优势,但由于它不能通过扩展来工作,因此它在 中失败\typeout
。
答案2
第一种解决方案与另外两种解决方案有两个不同之处:与第一种解决方案相比,第二种和第三种解决方案都使用尾部递归。与此相关的是,第一种解决方案必须将参数从迭代复制到迭代,而其他两种解决方案只传递参数一次,逐个字符地处理而不复制其余部分。
第一个解决方案。在传统的编程语言中,if-then-else-fi 会被完全读取,然后才会执行其中一个分支的代码。在 TeX 中,\fi
直到展开之前,它都会保留在输入中。因此,第一个循环会产生类似
\ifx\relax H\else(H)\xloop<ello...>\fi
(H)\ifx\relax e\else(e)\xloop<llo...>\fi\fi
(H)(e)\ifx\relax l\else(l)\xloop<lo...>\fi\fi\fi
注意\fi
末尾的 s 是如何堆积的。
第二种解决方案。这是最紧凑的一个。\expandafter
更改了扩展顺序:\fi
首先扩展,因此\ifx
在继续下一个之前先完成扩展\xloop
。
\xloop Hello...\relax
\ifx\relax H\else (H)\expandafter\xloop\fi ello...\relax
(H)\expandafter\xloop\fi ello...\relax
(H)\xloop ello...\relax
(H)\ifx\relax e\else (e)\expandafter\xloop\fi llo...\relax
(H)(e)\expandafter\xloop\fi llo...\relax
(H)(e)\xloop llo...\relax
第三种解决方案。 \afterassignment
首先在测试宏之前将下一个字符“赋值”给宏。尾部递归是通过让\next
指向结束之后要执行的代码来实现的\fi
。
\assignthencheck Hello...\relax
\xloop ello...\relax % \xchar is set to H
\ifx\relax\xchar\let\next=\relax\else(\xchar)\let\next=\assignthencheck\fi\next ello...\relax
(\xchar)\let\next=\assignthencheck\fi\next ello...\relax
(H)\next ello...\relax % \next points to the code of \assignthencheck
(H)\xloop llo...\relax % \xchar is set to e
(H)\ifx\relax\xchar\let\next=\relax\else(\xchar)\let\next=\assignthencheck\fi\next llo...\relax
(H)(\xchar)\let\next=\assignthencheck\fi\next llo...\relax
(H)(e)\next llo...\relax % \next points to the code of \assignthencheck
编辑:为了说明 David 和 Enrico 所说的前两个解决方案完全可扩展,而第三个解决方案不可扩展,请尝试
\edef\resultexpanded{\markletters{Hello World!}}
\edef
扩展第二个参数,直到遇到不可扩展的 TeX 基元(或字符),然后使用结果定义\resultexpanded
。对于 的前两个版本markletters
,\resultexpanded
定义为
(H)(e)(l)(l)(o)(W)(o)(r)(l)(d)(!)
而在第三种情况下,它包含
\afterassignment\let\next=\relax\next\let\xchar=Hello World!\relax
这是一种不可扩展的休息。
答案3
Tarass 和 David Carlisle 的解决方案并没有什么不同。后者效率更高,因为它在进入下一个循环之前删除了条件,而前者将它们嵌套起来,并且在每个循环中都会将一个\fi
添加到堆栈中:(非常)长的字符串可能会导致内存溢出。
另一个区别,即分隔符<>
,是不相关的,因为它们根本不起任何作用。
忽略空格是宏可扩展性的代价:这意味着结果是直接通过宏扩展“计算”的。
第三种解决方案使用不同的方法:不是将下一个项作为宏的参数吸收(这是造成空间吞噬的原因),而是将控制序列交给下一个项,然后将其删除,直到到达\relax
。
请注意,您不能使用它来存储已处理的令牌列表,因为所有循环都用于\xchar
打印该项目。
xparse
和 的进一步解决方案expl3
:
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\markletters}{om}
{
\IfNoValueTF{#1}
{
\kessels_markletters:nn { #2 } { \tl_use:N \l_kessels_marked_letters_tl }
}
{
\kessels_markletters:nn { #2 } { \tl_set_eq:NN #1 \l_kessels_marked_letters_tl }
}
}
\tl_new:N \l_kessels_unmarked_letters_tl
\tl_new:N \l_kessels_marked_letters_tl
\cs_new_protected:Nn \kessels_markletters:nn
{
\tl_set:Nn \l_kessels_unmarked_letters_tl { #1 }
\tl_replace_all:Nnn \l_kessels_unmarked_letters_tl { ~ } { \textvisiblespace }
\tl_clear:N \l_kessels_marked_letters_tl
\tl_map_inline:Nn \l_kessels_unmarked_letters_tl
{
\tl_put_right:Nn \l_kessels_marked_letters_tl { (##1) }
}
#2
}
\ExplSyntaxOff
\begin{document}
\markletters{Hello World!}
\markletters[\foo]{Hello World!}
\texttt{\meaning\foo}
\end{document}
空格首先被替换为(如果您愿意,\textvisiblespace
也可以使用)。\nonbreakingspace
如果缺少可选参数,则打印处理后的字符串,否则将其存储在作为参数给出的宏中(不检查它是否未定义,但可以轻松添加)。
这是一个简化版本,仅用于打印结果。如您所见,代码要短得多,并且每个命令都执行了明确规定的任务。它与 Florent 的代码完全相同。
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\markletters}{m}
{
% save the input in a variable
\tl_set:Nn \l_kessels_unmarked_letters_tl { #1 }
% replace spaces with \textvisiblespace
\tl_replace_all:Nnn \l_kessels_unmarked_letters_tl { ~ } { \textvisiblespace }
% map the input surrounding each item by parentheses
\tl_map_inline:Nn \l_kessels_unmarked_letters_tl { (##1) }
}
\tl_new:N \l_kessels_unmarked_letters_tl
\ExplSyntaxOff
\begin{document}
\markletters{Hello World!}
\end{document}