我刚刚从第一次真正尝试 LaTeX3 编程中恢复过来。这是……一次体验。不是完全不愉快的经历,一旦我习惯了语法,我就会发现它比看起来容易得多。我几乎忘记了我正在用宏语言编程,并开始再次以函数和变量的方式思考。
几乎。
我遇到的难题是辅助函数的概念。我有一个主函数,它做了很多工作。我很想把它的一部分交给其他函数,以便跟踪正在发生的事情。但我不知道如何正确地进行这种分离。
下面是一个例子。假设我想计算许多 2 向量的欧几里得长度。在 lua 中,我可能会这样写:
function veclen(a,b)
return math.sqrt(a^2 + b^2)
end
现在,这不能直接与 LaTeX3“函数”进行比较,所以让我假装无法在 lua 中编写内联公式,并将其写得更像 LaTeX3。
function veclen(a,b)
local s = a
local t = b
multiply(s,s)
multiply(t,t)
add(s,t)
sqrt(s)
return s
end
这里,multiply
和add
是“按照其名称执行”的函数,但至关重要的是,它们不返回值,而是将答案存储在第一个变量中。
这与我的 LaTeX3 函数非常相似:
\cs_new:Nn \fp_veclen:NNN {
\fp_set_eq:NN \l_hobby_veclena_fp #2
\fp_set_eq:NN \l_hobby_veclenb_fp #3
\fp_mul:Nn \l_hobby_veclena_fp {\l_hobby_veclena_fp}
\fp_mul:Nn \l_hobby_veclenb_fp {\l_hobby_veclenb_fp}
\fp_add:Nn \l_hobby_veclena_fp {\l_hobby_veclenb_fp}
\fp_pow:Nn \l_hobby_veclena_fp {.5}
\fp_set_eq:NN #1 \l_hobby_veclena_fp
}
关键的区别,也是我想问的,是范围。
在 lua 函数中,a
和b
是本地的,但即使它们不是,命令local s = a
也会local t = b
强制它们成为本地的。但是 会return s
突破函数范围并使答案在下一级可用。在 TeX 中执行此操作时,我会在一个组内进行所有计算:类似于:
\cs_new:Nn \fp_veclen:NNN {
\group_begin:
\fp_set_eq:NN \l_hobby_veclena_fp #2
\fp_set_eq:NN \l_hobby_veclenb_fp #3
\fp_mul:Nn \l_hobby_veclena_fp {\l_hobby_veclena_fp}
\fp_mul:Nn \l_hobby_veclenb_fp {\l_hobby_veclenb_fp}
\fp_add:Nn \l_hobby_veclena_fp {\l_hobby_veclenb_fp}
\fp_pow:Nn \l_hobby_veclena_fp {.5}
\group_end:
\fp_set_eq:NN #1 \l_hobby_veclena_fp
}
但这样做是行不通的:如果我把 放在 那里\group_end:
,那么\l_hobby_veclena_fp
它就失去了价值。如果我把它放在后那么该任务对于该组来说是本地任务,因此片刻之后就会丢失。
长话短说:我想要计算是本地的,这样我就可以毫无顾忌地使用临时变量,但我需要任务在计算组之外(但不是它是一个全局变量(.global),以便调用代码可以使用它。
我该怎么做?显然这是可能的,因为 LaTeX3 函数必须始终这样做[1]。TikZ 使用“走私”来做到这一点:定义一个临时全局变量作为答案,然后在组外进行分配,这使得实际分配不是全局的,而是在计算组之外:\global\let\tikz@smuggle=\the@answer\endgroup\let\the@answer=\tikz@smuggle
。正确的 LaTeX3 方式是什么?
为了加分,还有一个与作用域相关的小问题。在 lua 函数中,变量a
和b
已经是本地变量了:我可以毫无顾忌地重新赋值它们。在 TeX 中,这可不是件容易的事。如果我做了类似的事情
\fp_set_eq:NN \l_my_tmpa_fp #1
\fp_set_eq:NN \l_my_tmpb_fp #2
那么我使用以下方式调用函数总是存在危险\calc:NN \l_my_tmpb_fp \l_my_tmpa_fp
。我如何为传入变量创建本地别名没有为每个单独的函数定义一组新的临时宏?
[1] 我知道我可以看看 LaTeX3 代码 - 事实上我快速浏览了一下并有了个模糊的想法,但我怀疑如果我不问的话我会错过一些细微之处,我希望其他人能够从更公开的答案和解释中受益。
答案1
溶液 1
这可能被认为是小题大做,但我曾经编写过一个函数\group_after_set:NNn
来提供一个“抽象”来处理这个问题,用法如下:
\group_begin:
\group_begin:
\group_after_set:NNn \int_set:Nn \y {3}
% \y == 3
\group_end:
% \y == 3
\group_end:
% \y == undefined
我很喜欢它,但我不知道是否应该将其添加到 expl3。有什么建议吗?
这种方法的优点是您可以在一个组内多次使用它来“导出”多个变量,这与 Enrico 的答案不同,后者对于常见情况来说更加简单和有效。
无论如何,这里是:
\documentclass{article}
\usepackage{expl3}
\begin{document}
\ExplSyntaxOn
\cs_if_free:NT \group_insert_after:N
{
\cs_set_eq:NN \group_insert_after:N \group_execute_after:N
}
\cs_generate_variant:Nn \tl_if_empty:nT {v}
\cs_generate_variant:Nn \tl_show:N {v}
\cs_new:Nn \group_after_set:NNn
{
% set the variable locally for use inside the group:
#1 #2 {#3}
\cs_if_exist:cF { g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
{
\tl_new:c { g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
}
% first time the function is executed inside the group:
\tl_if_empty:vT { g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
{
% set up the aftergroup execution:
\group_insert_after:c
{ g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
% reset the material for aftergroup execution:
\tl_gset:cx { g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
{
\tl_gclear:c { g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
}
}
% append the new material to the aftergroup execution:
\tl_gput_right:cx
{ g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
{
\exp_not:n { #1 #2 } { \exp_not:V #2 }
}
}
\cs_generate_variant:Nn \group_insert_after:N {c}
\int_new:N \y
\int_new:N \yy
\cs_generate_variant:Nn \tl_if_eq:nnF {V}
\cs_new:Nn \assert:Nn
{
\tl_if_eq:VnF #1 {#2} {[ERROR:\tl_to_str:n{#1~!=~#2}]}
}
\group_begin:
\group_after_set:NNn \tl_set:Nn \x {xx}
\group_after_set:NNn \tl_set:Nn \xx {x}
\assert:Nn \x {xx}
\assert:Nn \xx {x}
\group_begin:
\group_after_set:NNn \int_set:Nn \y {3}
\group_after_set:NNn \int_set:Nn \yy {2}
\assert:Nn \y {3}
\assert:Nn \yy {2}
\group_end:
\assert:Nn \x {xx}
\assert:Nn \xx {x}
\assert:Nn \y {3}
\assert:Nn \yy {2}
% check repeated doesn't break anything:
\group_begin:
\int_set:Nn \y {6}
\int_set:Nn \yy {5}
\group_end:
\assert:Nn \x {xx}
\assert:Nn \xx {x}
\assert:Nn \y {3}
\assert:Nn \yy {2}
\group_end:
\assert:Nn \x {xx}
\assert:Nn \xx {x}
\assert:Nn \y {0}
\assert:Nn \yy {0}
\ExplSyntaxOff
\end{document}
编辑:添加一些注释并稍微清理一下代码。
解决方案 2(稍后添加)
从本页其他地方与 Ahmed 的讨论中,我清楚地认识到,关于 介绍的语法并不是一个好主意。例如,如果您想使用 设置变量tl_set:Nx
,那么使用同一函数在组外定义变量是不合适的。我们真正寻找的是 etextool 的扩展,\AfterGroup
允许轻松扩展控制(这是 expl3 整个设计理念的一部分)。
因此,从语法上讲,需要独立地分离本地变量和“组转义”变量。下面是上述代码的修改版实现。代码看起来类似:
\int_new:N \y
\group_begin:
\group_begin:
\int_set:Nn \y {3}
\group_var_return:NN \int_set:Nn \y
% \y == 3
\group_end:
% \y == 3
\group_end:
% \y == 0
请注意,由于我们没有变量类型和它们的设置函数之间的映射,因此在这种情况下\int_set:Nn
需要两次。这实际上并不是那么糟糕,因为理论上你可以将它用于各种设置函数。所以我在这里写的“一般”方法如下:
\clist_new:N \z
% ...
\group_begin:
% let's say we're in a macro right now that processes
% some input argument and saves the result to "\y".
\do_something_with:Nn \y {#1}
\group_after_insert:nV { \clist_put_right:Nn \z } { \y }
\group_end:
因此很容易看出前者(\group_var_return:NN
)就像是\group_after_insert:nV { \int_set:Nn \y } \y
。这些抽象层次是否必要或有用,这有点悬而未决。我倾向于认为至少这个\group_var_return:NN
看起来不错。
以下是全部内容的实现:
\documentclass{article}
\usepackage{expl3}
\begin{document}
\ExplSyntaxOn
\cs_if_free:NT \group_insert_after:N
{
\cs_set_eq:NN \group_insert_after:N \group_execute_after:N
}
\cs_generate_variant:Nn \tl_if_empty:nT {v}
\cs_generate_variant:Nn \group_insert_after:N {c}
\cs_new:Nn \group_after_insert:nn
{
\cs_if_exist:cF { g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
{
\tl_new:c { g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
}
% first time the function is executed inside the group:
\tl_if_empty:vT { g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
{
% set up the aftergroup execution:
\group_insert_after:c
{ g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
% reset the material for aftergroup execution:
\tl_gset:cx { g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
{
\tl_gclear:c { g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
}
}
% append the new material to the aftergroup execution:
\tl_gput_right:cx
{ g_aftergroup_ \int_use:N \etex_currentgrouplevel:D _tl }
{
\exp_not:n { #1 {#2} }
}
}
\cs_generate_variant:Nn \group_after_insert:nn {nV}
\cs_new:Npn \group_var_return:NN #1 #2
{
\group_after_insert:nV { #1 #2 } { #2 }
}
\int_new:N \y
\int_new:N \yy
\cs_generate_variant:Nn \tl_if_eq:nnF {V}
\cs_new:Nn \assert:Nn
{
\tl_if_eq:VnF #1 {#2} {[ERROR:\tl_to_str:n{#1~!=~#2}]}
}
\group_begin:
\tl_set:Nn \x {xx}
\tl_set:Nn \xx {x}
\group_after_insert:nV { \tl_set:Nn \x } { \x }
\group_after_insert:nV { \tl_set:Nn \xx } { \xx }
\assert:Nn \x {xx}
\assert:Nn \xx {x}
\group_begin:
\int_set:Nn \y {3}
\int_set:Nn \yy {2}
\group_var_return:NN \int_set:Nn \y
\group_var_return:NN \int_set:Nn \yy
\assert:Nn \y {3}
\assert:Nn \yy {2}
\group_end:
\assert:Nn \x {xx}
\assert:Nn \xx {x}
\assert:Nn \y {3}
\assert:Nn \yy {2}
% check repeated doesn't break anything:
\group_begin:
\int_set:Nn \y {6}
\int_set:Nn \yy {5}
\group_end:
\assert:Nn \x {xx}
\assert:Nn \xx {x}
\assert:Nn \y {3}
\assert:Nn \yy {2}
\group_end:
\assert:Nn \x {xx}
\assert:Nn \xx {x}
\assert:Nn \y {0}
\assert:Nn \yy {0}
\ExplSyntaxOff
\end{document}
答案2
有了新的 FPU,我们可以在这种特殊情况下完全避开群体(对于更一般的“逃离群体”问题,请参阅egreg 的回答)。
\cs_new_protected:Npn \fp_veclen:NNN #1#2#3
{
\fp_set:Nn #1
{
( ( #2 ) ^ 2 + ( #3 ) ^ 2 ) ^ ( 0.5 )
}
}
(在撰写本文时,这需要开发版本expl3
而不是 CTAN 版本。)
关于问题的“奖励”部分,我只需使用 -type 扩展传递值即可V
。TeX 分组不像 Lua 分组那样工作,因此您无法按照自己喜欢的方式进行分组。
答案3
您只需在关闭组之前获取变量的值;因为您必须跳过三个标记,所以\exp_args:NNNV
这就是您所需要的:
\cs_new_protected:Npn \fp_veclen:NNN #1 #2 #3
{
\group_begin:
\fp_set_eq:NN \l_hobby_veclena_fp #2
\fp_set_eq:NN \l_hobby_veclenb_fp #3
\fp_mul:Nn \l_hobby_veclena_fp {\l_hobby_veclena_fp}
\fp_mul:Nn \l_hobby_veclenb_fp {\l_hobby_veclenb_fp}
\fp_add:Nn \l_hobby_veclena_fp {\l_hobby_veclenb_fp}
\fp_pow:Nn \l_hobby_veclena_fp {.5}
\exp_args:NNNV \group_end: \fp_set:Nn #1 \l_hobby_veclena_fp
}
正如约瑟夫在评论中所说protected
,这应该是不可扩展的;Npn
多样性更有效率。
如果需要在组外设置更多变量,则必须使用不同的方法(等待 Will 的提议得以实施);定义中的最后一行可以变成
\use:x
{
\group:end
\fp_set:Nn \exp_not:N #1 { \l_hobby_veclena_fp }
% other similar assignments
}
(感谢 Joseph Wright 和 Marco Daniel 的评论。)
请注意,\fp_veclen:Nnn
如果您打算将其称为 ,则应该具有此签名\fp_veclen:Nnn \l_hobby_veclen_fp {2}{3}
。还\fp_set_eq:NN
应该是\fp_set:Nn
。
答案4
现在functional
包(基于expl3
)我们可以以类似于语言的方式进行编程Lua
。请注意,\Result
命令收集返回值并将其传递出函数组。
-- lua code for comparison --
function veclen(a,b)
return math.sqrt(a^2 + b^2)
end
\documentclass{article}
\usepackage{functional}
\Functional{scoping=true} % make every function become a group
\begin{document}
\IgnoreSpacesOn
\PrgNewFunction \veclen { M M } {
\Result { \FpEval { ( (#1)^2 + (#2)^2 ) ^ (0.5) } }
}
\FpSet \lTmpaFp {1.2}
\FpSet \lTmpbFp {3.4}
\veclen \lTmpaFp \lTmpbFp
\IgnoreSpacesOff
\end{document}
并且对局部变量的改变会被重置出函数之外。
\documentclass{article}
\usepackage{functional}
\Functional{scoping=true} % make every function become a group
\begin{document}
\IgnoreSpacesOn
\PrgNewFunction \veclenx { M M } {
\FpSet #1 {-1.2}
\FpSet #2 {-3.4}
\Result { \FpEval { ( (#1)^2 + (#2)^2 ) ^ (0.5) } }
}
\IgnoreSpacesOff
\FpSet \lTmpaFp {1.2}
\FpSet \lTmpbFp {3.4}
\veclenx \lTmpaFp \lTmpbFp
\string\lTmpaFp{} = \FpUse \lTmpaFp,
\string\lTmpbFp{} = \FpUse \lTmpbFp.
\end{document}