完全可扩展宏的优点和缺点

完全可扩展宏的优点和缺点

据我目前了解,完全可扩展的宏类似于函数式编程中的纯函数/无效果函数。相应地,不可扩展的东西,例如\def,是有效的计算——例如,它们不能在内部“评估” \edef。无效果函数通常是有益的(想想 OOP 中的不可变对象),我希望这可以延续到完全可扩展的 TeX 宏。

然而,编写完全可扩展的宏在很多情况下似乎很棘手(例如,使宏可扩展的技巧)此外,xparse软件包文档似乎表明完全可扩展的宏通常不是期望:

在极少数情况下,使用完全可扩展的参数抓取器创建函数可能会很有用。 [...] 此功能应仅在绝对必要时使用;如果您不了解何时可能使用此功能,请不要使用这些功能!

那么,什么时候应该使用完全扩展宏?完全扩展宏的优点和缺点是什么?

答案1

我认为最好不要将可扩展/不可扩展的区别与其他语言的概念进行比较。与扩展相关的主要问题实际上是 TeX 执行模型所特有的(有些人会说是奇特的)。

TeX 有两种主要的操作模式。所有赋值和装箱操作都发生在(在TeXBook术语)称为不可扩展操作。宏扩展发生在这之前,但与(比如说)C 预处理器的宏扩展不同,宏扩展和不可扩展操作必然交织在一起。

也许值得注意的是,所提出的问题并没有明确定义。

TeX 令牌可以是可扩展或者不可扩展但“完全可扩展”是一个灰色地带,充满了陷阱,粗心大意的人可能会掉入其中。

\def任何由(或等)定义的标记\newcommand都是按照定义可扩展

诸如此类的字符标记a根据定义是不可扩展的。

\def是不可扩展的令牌。

所以如果你定义

\def\zza{}
\def\zzb{a}
\def\zzc{\def\zze{}}
\def\zzd{\ifmmode a \else b\fi}

那么每个都是可扩展,分别进行扩展 <nothing> a \def\zze{} \ifmmode a \else b\fi

然而,其中哪一个是完全可扩展

显然\zza是。但如果“完全可扩展”的定义意味着可以反复扩展,不会留下不可扩展的标记那么唯一可以完全扩展的标记将全部扩展为无。

因此,大多数人会将其归类\zzb为完全可扩展的,即使它可以扩展到a不可扩展的程度。

因此,比“完全可扩展”更好(或至少更准确)的术语是“在仅扩展上下文中是安全的”。在和\edef\write当 TeX 寻找数字或维度时,以及其他几个地方,TeX仅有的进行扩展,不进行任何赋值或其他不可扩展的操作。

 \edef\yyb{\zzb}

当然是安全的,它与 相同\def\yyb{a}。因此\yyb在仅扩展的上下文中是安全的。

\edef\yyc{\zzc}

并不安全,它和

\edef\yyc{\def\zze{}}

现在\def不扩展,但在仅扩展上下文中,标记保持惰性,它不会做出定义,因此 TeX 会尝试扩展,\zze但通常尚未定义,因此会导致错误,或者如果\zze有定义,则会扩展,这几乎总是不受欢迎的行为。这是 LaTeX 中臭名昭著的“移动参数中的脆弱命令”错误的基本原因。

因此\zzc在仅扩展的上下文中是不安全的。如果它是由 e-TeX 构造定义的

\protected\def\zzc{\def\zze{}}

然后在仅扩展构造中,受保护的令牌变为不可扩展的,因此

\edef\yyc{\zzc}

那么将是安全的,并且与受保护的命令在仅扩展上下文中是安全的相同\def\yyc{\zzc},但由于这种安全性是通过使令牌暂时不扩展来实现的,因此说它“完全可扩展”可能并不准确。

\edef\yyd{\zzd}

\edef\yyd{\ifmmode a \else b\fi}

\def\yyd{b}

或者\def\yyd{a}如果定义发生在内部$...$(或方程显示)。类似地,它将扩展到b数组单元的开头,因为扩展将在 TeX 扩展寻找\omit\multicolumn)时发生,因此在插入之前$将数组单元置于数学模式。同样,这里需要一个受保护的定义来限制扩展。

因此,有时让事物变得可扩展是件好事,因为这样可以保留更多的选择。

\def\testa#1#2#3{%
  \ifnum#1=0
  \def\next{#2}%
  \else
  \def\next{#3}%
  \fi
  \next}

\def\firstoftwo#1#2{#1}
\def\secondoftwo#1#2{#2}

\def\testb#1{%
  \ifnum#1=0
  \expandafter\firstoftwo
  \else
  \expandafter\secondoftwo
  \fi}

如果是,则和\testa{n}{yes}{no}\testb{n}{yes}{no}将执行,否则将执行,但通过扩展工作,因此在仅扩展的上下文中是安全的(如果其参数是安全的)。该版本依赖于内部不可扩展的操作。(Plain TeX 和 LaTeX2.09 使用了许多使用 的测试,LaTeX2e 尽可能将它们更改为可扩展形式。)yesn0no\testb\testa\def\next\def\next

对于数字测试,使用可扩展形式很容易,但如果你想测试两个“字符串”是否相等,最简单的方法是

\def\testc#1#2{%
  \def\tempa{#1}\def\tempb{#2}%
  \ifx\tempa\tempb
  \expandafter\firstoftwo
  \else
  \expandafter\secondoftwo
  \fi}

但现在,尽管我们已经使用了\expandafter\firstoftwo 构造,但测试依赖于两个不可扩展的定义。如果你 真的需要以可扩展的方式进行测试,您可以在此网站上找到一些问题,但任何答案通常都充满了特殊条件和不起作用的情况,并且依赖于某种缓慢的逐个标记循环,通过两个参数测试它们是否相等。在 99% 的情况下,这种复杂化是不必要的,不可扩展的测试就足够了。如果您试图定义一组一致的测试(例如在包中ifthen\ifthenelse那么如果您接受某些测试必然是不可扩展的事实,那么您可以选择使它们全部不可扩展,以便它们以一致的方式运行。

所以答案是:

这一切都取决于……

答案2

虽然 TeX 是图灵完备的,但这并不能说明某个特定操作是否可以扩展执行。因此,在考虑某件事物是否可以扩展编码时,第一个要考虑的问题是它是否可能。正如在为什么并非所有事物都可扩展?,某些操作不可扩展,因为底层基元不可扩展。明显的例子包括赋值、分组和任何形式的框内排版。这意味着,例如,不可能以可扩展的方式测量材料的排版宽度,因此您无法使用类似

\newdimen\mydim
\mydim\wd\hbox{foo}%

尽管这看起来“合乎逻辑”,但必须采取相当于

\newdimen\mydim
\newbox\mybox
\setbox\mybox=\hbox{foo}%
\mydim\wd\mybox

(当您开始想要做更复杂的表达时,这一点会更加明显。)

手册中提到的第二个问题xparse是,可扩展方法通常存在一些限制,而这些限制在不可扩展方法中不会出现。一个特别值得注意的情况是提前查找可选参数。在不可扩展的方式中,这是使用\futurelet(赋值,因此不可扩展)来完成的。要以可扩展的方式执行相同的操作,您必须获取一个参数。该方法并不像我们有

\foo{[} ...

然后\futurelet将检测{,而参数抓取将看到[。假设我们正在寻找类似 LaTeX 的可选参数,则可扩展测试将是“错误的”。因此,这里的可扩展代码在处理方面受到更多限制。

编写可扩展代码通常比编写不可扩展代码要棘手得多。除了以已知方式将其留在输入流中之外,您无法保存任何内容,而这可能很难跟踪。这还会影响性能,因为对于复杂的操作,您可能需要跟踪大量内容。

这里相关的问题是,可扩展性有两种类型。在 中\edef,TeX 会不断扩展标记,直到只剩下不可扩展的标记。在这种情况下,你可以在输入流的前面添加“输出”标记,让 TeX 继续处理其余的操作。另一方面,你可以使用 进行可扩展扩展\romannumeral-`\q,但是,这会在第一个不可扩展标记处停止。因此,如果你想编写一个可以在这种“完整”扩展中工作的函数,你必须非常仔细地观察输出标记。(LaTeX3 语言expl3分别将这两种扩展形式区分为x- 和f-type,文档指出可扩展函数是否能在f-type 情况下正常工作。)

这可能会让您想知道为什么要编写可扩展函数。正如问题所指出的那样,这样做有好处。最明显的是它们可以在作业中使用,例如设置维度。Bruno Le Floch 为 LaTeX3 编写了一个可扩展的 FPU,这是一个很好的例子。TeX 还会在其他一些上下文中扩展标记,最明显的是在表格(\halign)单元格的开头查找\omit\noalign,如果您想(轻松)处理对齐问题,则需要使用可扩展方法。(还有其他方法可以做到这一点!)

当 TeX 写入文件时,它会进行完全扩展。对于给定的宏,这可能是理想的,也可能不是理想的。例如,许多 LaTeX 宏在写入文件时没有进行扩展,.aux因为这是扩展的“错误”时间。

一般来说,除了编程层(如果可能的话,您确实希望具有可扩展性)之外,创建可扩展宏通常不是优先事项,因此在 中有声明xparse。总的来说,当我知道需要可扩展函数时,我会考虑创建它。

答案3

也许两个主要的应用是:

  • 当宏将在内部使用时\csname...\endcsname
  • 当具有 时的当前值时\edef,标记等是必要的。

答案4

其他一些观点,我认为其他答案中没有提到或只是简要提到。

只有完全可扩展的函数才能方便地“返回结果”作为另一个函数的参数。

假设你有一个函数\increaseone{5},执行时会进行排版6

现在假设您想要调用\dosomethingwith{\increaseone{5}} ,并且希望它与相同。这只有当完全可扩展(并完全扩展其参数)时\dosomethingwith{6}才有可能(*) 。\increaseone\dosomethingwith

如果不是这种情况,您通常需要手动将它们存储到临时“变量”中,例如

\increaseone[store-to=\tmp]{5}
\dosomethingwith{\tmp}

最后,为了编程方便(†),人们倾向于尝试使“返回结果”的函数可扩展(expl3 中的⋆ 类型函数),如果它不会显著降低性能(当确实如此时,他们会尝试至少使其为✩ 类型可扩展)。

(*)从技术上讲,外部函数和内部函数也可以“同意”某个“中间”位置来传递结果,即内部函数将结果存储在那里,然后外部函数从中读取结果。请参阅functional包以了解该方法的示例。

(†)并且仅用于编程方便。您可以使用完全可扩展的宏执行所有操作,您可以使不可扩展的宏设置中间“变量”(或使用回调等),就像上面的方法一样。除非您使用某个库提供仅接受完全可扩展函数的“API”。

(其他一些扩展性更方便的情况)

tabular环境为例。

\begin{tabular}{|c|c|}
    \int_step_inline:nn {5} {
        #1 & #1 \\
    }
\end{tabular}

此代码将打印一个额外的单元格,如下所示。(假设您设置了正确的 catcode)

输出

原因在于,在低级tabular实现中,halign使用了一些复杂的逻辑来确定是否存在单元格,如果有不可扩展的东西,则确定存在。

例如,以下代码产生相同的输出

\begin{tabular}{|c|c|}
    1 & 1 \\
    2 & 2 \\
    3 & 3 \\
    4 & 4 \\
    5 & 5 \\
    \relax
\end{tabular}

参考:完全可扩展的循环宏,也适用于表格

再次提醒,不要求可扩展性,只是更方便。使用可扩展性\int_step_function:nN可以解决这个问题,但代码也可以重写,无需可扩展性,如下所示

\tl_set:Nn \__content_to_be_executed {}
\int_step_inline:nn {5} {
    \tl_put_right:Nn \__content_to_be_executed { #1 & #1 \\ }
}

\begin{tabular}{|c|c|}
    \__content_to_be_executed
\end{tabular}

如何消除增加的渐近复杂性是读者的练习。

LuaTeX 中的变化。

首先,有新的原语\immediateassignment,它消除了上面提到的仅扩展编程所导致的所有缓慢问题。

然后,还有\directlua可扩展的,并允许您使用 Lua 来存储它的东西。(虽然 Lua 和 TeX 都使用哈希表,但使用 Lua 哈希表的优点是不需要为每个表设置“通用唯一前缀/后缀”,从而加快了哈希/检索速度。当然,编程也更方便。+ 在 Lua 中处理不平衡的标记列表更快,因为您需要 TeX 中的中间表示 + TeX 字符串哈希函数存在一些问题,请参见上文)

有些事情只能以不扩展的方式完成。(更多示例。)

  • 上面提到了解析可选参数。您无法区分\function {[} something ]\function [ something ]扩展。(公平地说,如果您假设以下标记是 a{或 a,[那么您可以将其字符串化,但一般问题仍然存在)

  • 其他值得一提的是,区分某些 other-catcode 字符(例如!)与其 active-catcode 版本(即\let其 other-catcode)。(例如,\let <active token !> = <other token !> 除了定义使用 fork 模式的宏之外,每 256 个字符

    (因为仅扩展模式是 TeX 的一个特性,所以这种限制也是 TeX 的一个特性。请参阅下面的 LuaTeX。)

  • 使用给定的 charcode/catcode 组合生成一个字符标记。同样,除了为 256 个字符中的每一个定义宏之外,或者使用一些 XeTeX/LuaTeX 原语。

  • a任何涉及排版的事情。如果不设置框,就无法“获取字符的宽度”,因为框是不可扩展的。

(与这个问题无关的旁注,有些事情也是无法扩展的。例如

  • 检查以下标记是否已冻结 catcode(据我所知,这在 LuaTeX 中仍然无法完成)
  • 完全可靠地实现peek_analysis_map_inline函数(这可以在 LuaTeX 中完成)

例如这样的程序

%! TEX program = lualatex
\documentclass{article}
\begin{document}

\ExplSyntaxOn

\begingroup
\catcode`\_=12
\global\let\underscoreCatcodeOther _
\endgroup

\let \c \c_group_begin_token

\peek_analysis_map_inline:n { \tl_show:n { #1 } } \c_group_begin_token \underscoreCatcodeOther


\ExplSyntaxOff

\end{document}

\peek_analysis_map_inline:n会误以为下面的标记是\c

仅扩展编程速度较慢。(更多细节。)

这是因为在仅扩展编程中,内存模型有点类似于堆栈机(‡)(只有一个输入流供您将数据推送到其中并从中弹出数据。因此,您必须“跳过”堆栈的“前面”才能访问下面的数据,而且速度很慢)

例如,可以使用 toks 寄存器(或使用用数字索引的控制序列)在 O(n) 时间内反转标记列表(由所有支撑组组成)中的所有项目有一些性能警告),但据我所知,只有在不使用 LuaTeX 的情况下,才能在 O(n√n) 的扩展时间内实现该操作,或者在假设可用的情况下,在 O(n log n) 的扩展时间内实现该操作\expanded。 expl3 实现的\tl_reverse_items:n时间复杂度为 O(n²)。

(‡)从技术上讲,还有一些其他技巧可以将数据存储在其他地方,例如,当您执行时,\number 12345\<some expandable things>12345会“存储”在可扩展内容之前,或者当您执行时\expandafter \somecontrolsequence \<some expandable things>,您可以将去标记化的标记列表打包到控制序列中等。然而大多数时候这是不可能的。

即使\expanded包含在内,您也有一个“堆栈”和“另一个将令牌推送到的位置的堆栈”,尽管如此,内存模型仍然有限并且不如 tok 寄存器那么强大。

“完全可扩展”的含义。(澄清。)

这仅对于预期“返回结果”的“函数”才“有意义”。

例如,如果一个函数应该排版某些内容或设置某个全局变量,那么它实际上没有必要(§)完全可展开,因为完全展开不会产生任何所需的“结果”。因此它通常受到保护。

但是,计算某些值(例如加法等)的函数通常应该是可扩展的。如果无法以可扩展的方式实现,通常会有一个函数将其存储到某个“变量”中,以便稍后检索。

条件/映射函数通常是这里的难点。对于可扩展条件,您期望(伪代码)\edef \a { \ifsomething 1 \else 2 \fi }1或分配2给“变量” \a,但这仅在\ifsomething可扩展时成立。

否则您需要\ifsomething \edef \a {1} \else \edef \a {2} \fi(再次,伪代码)。

(§)除了 LuaTeX 之外,这无法以任何方式扩展实现。


边注。

关于记忆模型仅扩展编程(上面简要提到过,第 4 节“仅扩展编程较慢”),TeX 缓慢,以及如何使引擎更快地解释 TeX。

首先,我们知道,当这样的事情发生时……

\def \a #1 {\b {#1}}
\def \b #1 {\c {#1}}
\def \c #1 {\d {#1}}

每次函数调用都需要重新扫描,这花费的时间与中的标记数量呈线性关系#1

因此,目前不可能在欧拉(1)或者O(log N)\relax每个操作的时间(忽略将控制序列设置为或解释“标志”的可能性)

理论上,TeX 可以跟踪}每个匹配项{以快速加速到该点(以及}输入流上的悬而未决的问题,如果有的话),但也存在一些复杂情况......

  • 为了节省内存,TeX 标记列表在内部以链表的形式进行跟踪,并且⟨replacement text⟩可能在多个地方多次重复使用。因此,可能与多个不同的标记}匹配。{
  • 在扫描过程中,需要检查\outer支撑组中是否有任何标记。因此,每当某个标记改变其状态时(希望这种情况很少见),就需要重新扫描整个输入流(至少是被标记化的部分),以确定支撑组中\outer是否有标记。\outer
  • 对于在 之后生成的标记类似\noexpand。参数扫描应该将它们恢复为正常标记。
  • 与 . 类似\par(尽管这个可能更容易,因为它是通过标记而不是含义匹配的?不确定。)
  • 对于括号黑客,实现时需要小心,以正确匹配{重新插入的标记。(这部分可能不是很难,因为悬垂列表}被保留了下来)

除此之外,如果有一些 TeX 实现可以进行参数扫描欧拉(1)在非极端情况下,类似随机存取存储器的东西可以在可扩展的 TeX 编程中实现,只需O(log N)开销(其中是“记忆单元”的数量,通过维护例如平衡二叉树来实现。

这样的实现将大概加快现有代码的速度,尽管我不确定能提高多少,因为许多现有代码都是在假设参数扫描需要在);此外,还需要持续花费时间跟踪支架平衡。

我不确定是否可以进行参数扫描欧拉(1)全部不管有没有案例,但它似乎至少,这不需要\outer和令牌的复杂化。\noexpand

附注:我相信,如果不是一遍又一遍地重复使用同一个存储单元(当时,内存非常有限),它们实际上复制,时间复杂度将保持不变(*),但由于内存局部性,TeX 的运行速度会更快。当然,会存在一些极端情况的代码成倍增加内存比旧的实现更多。

(*):我认为目前的实现是这样的。如果实现使用智能参数抓取,则需要仔细考虑。

相关内容