[Tex/LaTex] How to get a value out of a group

expl3groupinglatex3programming

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.

Best Answer

Soln 1

This might be considered overkill, but I once wrote a function \group_after_set:NNn to provide an "abstraction" to handle this, used as in:

\group_begin:
  \group_begin:
    \group_after_set:NNn \int_set:Nn \y {3}
    % \y == 3
  \group_end:
  % \y == 3
\group_end:
% \y == undefined

I quite like it but I don't know if should be added to expl3 or not. Suggestions?

The advantage over this approach is that you can use it multiple times within a group to "export" multiple variables, unlike Enrico's answer which is much more simple and efficient for the common case.

Anyway, here it is:

\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}

Edit: add some comments and clean up the code slightly.

Soln 2 (added later)

From some discussion elsewhere on this page with Ahmed, it's become clear to me that the syntax introduced about isn't such a great idea. For example, if you want to set a variable using tl_set:Nx, it's not then appropriate to use that same function to define the variable outside the group. Really what we're looking for is an extension of etextool's \AfterGroup that permits easy expansion control (which is part of the whole design philosophy of expl3).

So separating the local and "group escaped" variables need to be done independently, syntax-wise. Here's a modified implementation of the above that does this. The code looks similar:

\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

Note that since we don't have a mapping between variable types and their setting functions, in this case \int_set:Nn is required twice. This isn't so bad, really, since you could in theory use this for all sorts of setting functions. So the "general" approach here I've written as follows:

\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:

So it's easy to see how the former (\group_var_return:NN) is just something like \group_after_insert:nV { \int_set:Nn \y } \y. Whether these levels of abstraction are necessary or helpful is a bit of an open question. I'm inclined to think that at least the \group_var_return:NN one looks good.

Here is the implementation of all that:

\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}
Related Question