I've just recovered from my first real foray into LaTeX3 programming. It was ... an experience. Not a completely unpleasant one and once I'd gotten used to the syntax then I found it a lot easier than it looks. I almost was able to forget that I was programming in a macro language and start thinking in terms of functions and variables again.
Almost.
Where I came up against a brick wall was in the concept of auxiliary functions. I had a main function that did a lot of work. I would quite like to have farmed off parts of it to some other functions, as much for keeping track of what was going on as anything else. But I couldn't work out how to do that separation properly.
Here's an example. Suppose I wanted to compute the Euclidean length of a lot of 2-vectors. In, say, lua then I might write:
function veclen(a,b)
return math.sqrt(a^2 + b^2)
end
Now that isn't directly comparable with a LaTeX3 "function" so let me pretend that I can't write inline formulae in lua and write it out a bit more like 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
Here, multiply
and add
are functions that "do what they say on the tin" but, crucially, instead of returning a value they store the answer in the first variable.
That's fairly similar to my LaTeX3 function:
\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
}
The crucial difference, and what I'm asking about, is the scoping.
In the lua function, a
and b
are local but even if they are not then the commands local s = a
and local t = b
force them to be local. But the return s
breaks out of the function scope and makes the answer available at the next level up. In doing this in TeX, I'd do all my computations inside a group: something like:
\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
}
Except that that wouldn't work: if I put the \group_end:
where I have done so then \l_hobby_veclena_fp
has lost its value. If I put it after the assignment then the assignment is local to the group and so is lost moments later.
To cut a long story short: I want the computation to be local so that I can use my temporary variables with impunity, but I need the assignment to be outside the computation group (but not global) so that it can be used by the calling code.
How do I do this? Obviously it is possible because the LaTeX3 functions must do this all the time[1]. TikZ uses "smuggling" to do this: define a temporary global variable to be the answer and then outside the group make the assignment which makes the actual assignment not global but outside the computation group: \global\let\tikz@smuggle=\the@answer\endgroup\let\the@answer=\tikz@smuggle
. What's the right LaTeX3-way to do this?
For bonus points, there's another small issue with scoping. In the lua function, the variables a
and b
were already local: I could reassign them with impunity. In TeX, that's not so easy. If I do something like
\fp_set_eq:NN \l_my_tmpa_fp #1
\fp_set_eq:NN \l_my_tmpb_fp #2
then there's always the danger that I called my function with \calc:NN \l_my_tmpb_fp \l_my_tmpa_fp
. How do I make local aliases for my incoming variables without defining a new set of temporary macros for every single function?
[1] I know I could look at the LaTeX3 code - indeed I took a quick glance and got a vague idea, but I suspect that there would be subtleties I'd miss by not asking, and I hope that others will benefit from a more public answer and explanation.
\fp_gset_eq:NN #1 \l_hobby_veclena_fp
inside the group.\l_hobby_veclena_fp
. But#1
was (or could have been) only a local variable in the calling function. So it might have another global value that I don't want to touch.\edef\@tempa{\endgroup\value=\the\value\relax}\@tempa
to move a value (counter, length) out of a group. Don't ask me how to translated that into Klingon, aeh, LaTeX3. The same is possible for macros:\edef\@tempa{\endgroup\def\noexpand\mymacro{\mymacro}}\@tempa
or, if you don't want to expand it all the way:\def\@tempa{\endgroup\def\noexpand\mymacro{\unexpanded\expandafter{\mymacro}}}\@tempa
, or without e-TeX:\expandafter\endgroup\expandafter\def\expandafter\mymacro\expandafter{\mymacro}
. The first two values also work nicely with multiple macros/registers.