使宏可扩展的技巧

使宏可扩展的技巧

可扩展宏很有用(我发现在狮子嘴里工作非常酷)。但它们很难编写。

更有经验的用户能否给出有助于实现可扩展性的提示?

例如,我们不能使用计数器,因为扩展时无法更改其值。但是,一个技巧可能是将计数器的值保留为A我们在扩展时移动的(比如说)的数量:5将被“存储”为AAAAA,我们可以通过将两个列表A一起移动来添加计数器,等等。当然,这并不高效,但毕竟它是可扩展的。

为了明确起见,假设我想定义一个宏,它的参数是分隔例如\verb:第一个字符标记决定了结束字符是什么,因此\foo|...|\foo'...'\foo+...+等中的任何一个都被视为相同。我可以用可扩展的方式来做到这一点吗?

欢迎使用任何其他技巧。

答案1

关于如何编写可扩展宏的问题,并没有一个正确的答案,因此我将这个问题设为 CW,也许其他人也会有兴趣做出贡献。

  • 尽可能使用 TeX 灵活的宏参数解析机制,而不是逐个字符地解析输入(如果使用 则不可扩展\futurelet)。

  • 将条件语句分成单独的宏。例如,如果您想测试参数标记是否是某个特定标记,则可以使用\ifx\foo#1 ...\else ...\fi,但这会在输入流中引入额外的标记。更好的方法是使用, \def\iffoo#1{\ifx#1\foo\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi}这样就不会留下任何额外的标记需要处理。(Herbert 写了一些类似的代码,将所有文本都收集到,\fi这很聪明,但我认为这更清楚。)它也可以很好地嵌套。

  • 有时使用中央公共服务

  • 在一些情况下,标记的扩展是其参数的完全扩展。例如,\csname ...\endcsname将完全扩展...。这可用于计算可以使用可扩展地恢复的字符标记字符串\string

     \expandafter\expandafter\expandafter\stripslash
         \expandafter\string\csname\foo\bar\baz\endcsname
    

    这确实会丢失 catcode,因为所有非空格的 catcode 都是 12,而空格的 catcode 是 10。在其他情况下,\romannumeral-`X\foo可以使用此技巧继续扩展,\foo直到达到不可扩展的标记。不过,它会吞掉空格标记。

  • 使用ε-TeX 扩展,如\numexpr ...\relax,算术可以相当容易地扩展执行。TeX 的截断与\divideε-TeX 的/,但这可以通过试验乘法和来解决\ifnum

答案2

这是对该问题中具体挑战的部分答案。

\def\foo#1{\fooo#1{}}
\def\fooo#1#2#3{\ifx#1#3\unelse\bar{#2}\else\unfi\fooo#1{#2#3}\fi}
\def\unelse#1\else#2\fi{\fi#1}
\def\unfi#1\fi{\fi#1}
\def\bar#1{\message{\string\foo{#1}}}
\foo:abc:
\foo!def!
\foo|gh{i|j}|
\bye

请注意最后一个例子中的括号如何屏蔽了括起来的条,使其不至于限定宏调用;然而,在这个过程中括号被剥离了。

答案3

如果您在 TeX 中实现使用变量的算法,则需要某种存储管理来存储变量的值。

在宏编程级别,变量的“值”无论如何都是标记集(控制序列标记/显式字符标记)。

如果可扩展性不是问题,您可以使用暂存宏或暂存标记寄存器,它们的扩展会产生构成变量值的标记。每次要检索变量的值时,都要应用扩展技巧来获取相关暂存宏的扩展/相关\the暂存标记寄存器的扩展。每次需要为变量分配另一个值时,都需要执行TeX 赋值( \def/ \renewcommand/ /...)。\MyScratchTokenRegister={...}

如果可扩展性是一个问题,您可以用尾递归的方式实现算法:算法每一步结束时的宏都会通过再次调用自身来终止,以执行算法的下一步。宏处理参数,每个参数表示一个变量,作为参数提供的标记表示变量的值。算法的每一步都是通过提供标记集作为自调用宏的下一次调用的参数来确定下一步变量的值。这里,算法的每一步都需要扩展和翻转标记流中的参数技巧,以便按自调用宏所需的顺序排列标记/参数,但扩展技巧/翻转宏参数通常不会永久占用内存。

可扩展性经常被忽视的一个问题是,了解需要触发多少个扩展步骤才能获得结果通常会很方便:你可以让 TeX 触发\romannumeral-expansion 以确保在任何情况下都只需要触发两个扩展步骤。第一个扩展步骤触发顶层扩展,产生一组由标记 引导的标记\romannumeral。第二个扩展步骤触发\romannumeral\romannumeral反过来,触发收集属于 TeX-number-quantity 的标记,以小写罗马符号表示,从而扩展可扩展标记。如果要转换的 TeX-number-quantity 具有非正值,则\romannumeral默默地吞下形成该 TeX-number-quantity 的标记而不返回任何标记。因此,只需让你的可扩展尾递归宏在其一个参数中收集形成结果的标记。当谈到终止时,在算法要传递的标记前面加上形成非正值的 TeX-number-quantity 的标记。

假设您希望定义一个宏,其中累积了 100 行文本以便\message可以显示它们。

变体 1-\linecollectloop执行多项任务:

\def\linescollected{}%
\def\linecollect#1#2{%
  \def\linescollected{}%
  \def\numberofthisline{#1}%
  \def\upperbound{#2}%
  \linecollectloop
}%
% macro \numberofthisline - number of current message-line
% macro \upperbound - number of last message-line to be printed
% macro \linescollected - message-lines gathered so far
\def\linecollectloop{%
  \edef\linescollected{\linescollected Line \numberofthisline.^^J}%
  \ifnum\numberofthisline<\upperbound\relax
    \edef\numberofthisline{\number\numexpr\numberofthisline+1\relax}%
    \expandafter\linecollectloop\fi
}%
\linecollect{1}{100}%
\show\linescollected
\message{\linescollected}%
\documentclass{article}
\begin{document}
\end{document}

变体 2 -\linecollectloop终止时执行一次分配:

\def\fot#1#2{#1}%
\def\sot#1#2{#2}%
\def\linecollect#1#2#3{%
  % #1 - number of current message-line
  % #2 - number of last message-line to be printed
  % #3 - message-lines gathered so far
  \ifnum#1>#2 \expandafter\sot\else\expandafter\fot\fi
  {\expandafter\linecollect\expandafter{\number\numexpr#1+1\relax}{#2}{#3Line #1.^^J}}%
  {\def\linescollected{#3}}%
}%
\linecollect{1}{100}{}%
\show\linescollected
\message{\linescollected}%
\documentclass{article}
\begin{document}
\end{document}

变体 3 -\linecollectloop根本不进行任何分配,完全可扩展并可与 -expansion 结合使用,以便您确切地知道通过获取结果\romannumeral需要触发的扩展步骤的数量:\expandafter

\def\fot#1#2{#1}%
\def\sot#1#2{#2}%
\def\linecollect#1#2#3{%
  % #1 - number of current message-line
  % #2 - number of last message-line to be printed
  % #3 - message-lines gathered so far; at initialization tokens
  %      can be prepend forming a non-postive <number>-quantity that
  %      terminates \romannumeral-expansion.
  \ifnum#1>#2 \expandafter\sot\else\expandafter\fot\fi
  {\expandafter\linecollect\expandafter{\number\numexpr#1+1\relax}{#2}{#3Line #1.^^J}}%
  {#3}%
}%
\expandafter\def\expandafter\linescollected\expandafter{\romannumeral\linecollect{1}{100}{0 }}%
\show\linescollected
\message{\linescollected}%
\documentclass{article}
\begin{document}
\end{document}

\exp:wexpl3 是\romannumeral并且\exp_end:expl3 是 TeX-⟨数字⟩\exp:w- 终止-expansion/ -expansion的非正值数量\romannumeral。由于它是一个控制字标记,根据诸如/ 之\chardef类的东西来定义,因此不会像 TeX- 的情况那样对其产生影响\uppercase\lowercase⟨数字⟩-由诸如 之类的明确字符标记形成的数量。0⟨space⟩

可扩展性的另一个经常被忽视的问题是处理应发送到控制台/shell/终端或 .log 文件的消息:使用传统的 TeX 引擎,无法以可扩展的方式触发写入消息。因此,使用可扩展宏/宏机制时,您有时会发现这样的做法:存在另一个参数,用户需要指定一些应在终止时作为结果传递的标记,以防发生(错误)情况,而可扩展宏机制实现的算法无法以合理的方式处理这种情况。


在运行 LaTeX 的 8 位 TeX 引擎上,默认加载 inputenc 包和 utf8 选项,对以 UTF8 编码的多字节字符进行字节标记化通常会产生多个字符标记,而不是一个。

在当今时代,如果使用 LaTeX 底层的 8 位 TeX 引擎处理 utf8 编码的文件,则可能存在一个陷阱,即尝试处理字符序列,就好像每个字符都由一个可以作为无分隔参数抓取的单个字符标记来表示一样。


在重复/复制事物的情况下,你经常可以应用\romannumeral在 TeX 后面附加三个尾随零后应用⟨数字⟩-quantity 乘以 1000 从而得到小写字母m与 TeX 值对应的数量的小写字母-⟨数字⟩-数量。然后,您可以用某种方式让 TeX 以尾部递归方式迭代,直到所有这些m都被消耗掉。

以下示例使用/ -trickery\replicate进行初始化,以获取尾随的。用于将这些内容作为对的调用的尾随项放置。 依次处理三个参数,第一个参数表示应复制的标记,第二个参数用于累积复制,第三个参数可以是字符或控制字标记,从而表示不再在第二个参数中进行累积迭代,但应传递第二个参数中累积的包含迄今为止收集的复制的内容:\replicateloop\romannumeral000m\relax\exchg\replicateloop\replicateloopm\relax\relax

%define a TeX-number-quantity of non-positive value
\chardef\stopromannumeral=`\^^00
\long\def\fot#1#2{#1}%
\long\def\sot#1#2{#2}%
\long\def\exchg#1#2{#2#1}%
\long\def\replicate#1#2{%
  % #1 amount of replications
  % #2 tokens to replicate
  \romannumeral\expandafter\exchg\expandafter{\romannumeral\number\number#1 000\relax}{\replicateloop{#2}{}}%
}%
\long\def\replicateloop#1#2#3{%
  % #1 tokens to replicate
  % #2 replications gathered so far
  % #3 lowercase letter m or \relax
  \ifx#3\relax\expandafter\sot\else\expandafter\fot\fi
  {\replicateloop{#1}{#2#1}}%
  {\stopromannumeral#2}%
}%
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{\replicate{10}{X}}%
\message{\meaning\temp}
\csname stop\endcsname % <- this terminates in case latex is in use
\bye % <- this terrminates in case TeX is in use

\replicate此示例在触发两个扩展步骤后提供结果:
第一个扩展步骤产生带有前导的顶层扩展\romannumeral
第二个扩展步骤触发\romannumeral-expansion,进而触发其他所有内容。

这会将十个字母X作为消息发送到控制台。

如果您不需要控制扩展步骤的数量来获得结果,您可以执行以下操作:

%define a TeX-number-quantity of non-positive value
\chardef\stopromannumeral=`\^^00
\long\def\foo#1{#1}%
\long\def\noo#1{}%
\long\def\exchg#1#2{#2#1}%
\long\def\replicate#1#2{%
  % #1 amount of replications
  % #2 tokens to replicate
  \expandafter\exchg\expandafter{\romannumeral\number\number#1 000}{\replicateloop{#2}}%
}%
\long\def\replicateloop#1#2{%
  % #1 tokens to replicate
  % #2 lowercase letter m or \relax
  \ifx#2\relax\expandafter\noo\else\expandafter\foo\fi{#1\replicateloop{#1}}%
}%
\message{\replicate{10}{X}}
\csname stop\endcsname % <- this terminates in case latex is in use
\bye % <- this terrminates in case TeX is in use

关于\romannumeral\number\number#1 000-construct:

我在查看一些代码时了解到了这个结构”用罗马数字迭代” 由 David Kastrup 撰写并发表于珍珠 2005 部分网络存在GUST(波兰 TeX 应用系统组)

%%% David Kastrup: Iterating with roman numeral
% Appendix D in the \TeX book has the task of defining \verb+\asts+ as a
% macro containing \verb+\number\n+ copies of an asterisk.  The
% solutions in the TeXbook are not really fun.  Here is one that is all
% of fun, efficient and simple:

\def\asts#1{\if#1m*\expandafter\asts\fi}
\edef\asts{\expandafter\asts\romannumeral\number\n 000\relax}

% Now for something more general: we want a macro \verb+\replicate+ that
% gets a number in its first argument and arbitrary tokens in its second
% argument and expands to the given number of repeated token strings.

% It is surprisingly hard to pass \emph{both} the shrinking string of
% \verb+m+ as well as the argument to repeated in a useful way into the
% expanding first macro, and the reader is advised to try it.  What I
% came up with was

\long\def\gobble#1{}
\long\def\xii#1#2{\if#2m#1\expandafter\xii\else\expandafter\gobble\fi{#1}}
\long\def\xiii#1\relax#2{\xii{#2}#1\relax}
\def\replicate#1{\expandafter\xiii\romannumeral\number\number#1 000\relax}

% A somewhat wittier variant that takes its toll on the semantic nest
% size would be

\def\recur#1{\csname rn#1\recur} \long\def\rnm#1{\endcsname{#1}#1}
\long\def\rn#1{}
\def\replicate#1{\csname rn\expandafter\recur
  \romannumeral\number\number#1 000\endcsname\endcsname}

% Of course, if we are leaving the area of \TeX\ compatibility and take
% a look at what we can do with \eTeX, we arrive at the boring

\def\replicate#1#2{\ifnum#1>0 #2%
  \expandafter\replicate\expandafter{\number\numexpr#1-1}{#2}\fi}

请注意,该构造\romannumeral\number\number#1 000适用于 TeX-⟨数字⟩#1-在收集过程中和使用 TeX 时,尾随空格被丢弃的数量-⟨数字⟩-数量#1-在收集过程中和使用 TeX 时不会丢弃尾随空格的⟨数字⟩- 需要尾随空格的数量#1。无论如何\romannumeral都会触发第一个数字\number,进而触发第二个数字。

例如,使用#1=10可得到。 第二个删除空格:。 第一个仅提供数字序列:。提供:。\romannumeral\number\number10⟨space⟩000
\number\romannumeral\number10000
\number\romannumeral10000
\romannumeralmmmmmmmmmm

例如,使用\newcount\mycount \mycount=10#1=\mycount可得到。 第二个传递的值是:。 第一个删除空格:。传递的值是:。
\romannumeral\number\number\mycount⟨space⟩000
\number\mycount
\romannumeral\number10⟨space⟩000
\number\romannumeral10000
\romannumeralmmmmmmmmmm

例如,使用\count12=10#1=\count12可得到。 第二个传递的值,其中收集寄存器的数字并删除空格:。 第一个仅传递数字序列:。传递:。
\romannumeral\number\number\count12⟨space⟩000
\number\count12\romannumeral\number10000
\number\romannumeral10000
\romannumeralmmmmmmmmmm

相关内容