13

I am still struggling with understanding the f-type expansion. What is it all about? The explanation on page 2 in interface3.pdf is not really satisfying.

In the given example

\tl_set:Nn \l_mya_tl { A }
\tl_set:Nn \l_myb_tl { B }
\tl_set:Nf \l_mya_tl { \l_mya_tl \l_myb_tl }

, how can a check that the content of \l_mya_tl is actually A\l_myb_tl?

Does it matter that \l_mya_tl is re-used in order to be set on the third line, and not another, hitherto unused token list variable, say \l_myc_tl?

Why does expansion stop after expanding \l_mya_tl as it is expandable after all?

Is there any thinkable scenario where f-expansion would continue after expanding the first token (\l_mya_tl, here)? How would \l_mya_tl need to be crafted in order to not interrupt further expansion?

Why would someone want to use f-expansion, which stops at some unpredictable place, when the argument is expected to be really fully expanded? (This is what f as in "fully" means to me.)

AlexG
  • 54,894

3 Answers3

12

f-type expansion ends upon finding an unexpandable token; if this token is a space (character code 32, category code 10) it is gobbled.

Your \tl_set:Nf \l_mya_tl { \l_mya_tl\l_myb_tl } will first do recursive expansion of \l_mya_tl, leading to A. This is unexpandable, so the business stops here. The token list to assign is evaluated to A\l_myb_tl and \l_mya_tl is updated to contain this list.

Changing the contents of \l_myb_tl will also change the expansion of \l_mya_tl, because this one contains a pointer to \l_myb_tl, rather than the value this variable had at definition time.

If you want to freeze the value of the updated \l_mya_tl variable to the values of \l_mya_tl and \l_myb_tl you have to use either x-type or e-type expansion.

These last two types lead to the same result, but with a big difference: e-type expansion can appear in expansion contexts, x-type cannot. Not so much of a difference in this case, because you're doing an assignment. Actually, there is no predefined \tl_set:Ne function, because it turns out that \tl_set:Ne would take twice as much time as needed by \tl_set:Nx.

\documentclass{article}
\usepackage{expl3,l3benchmark}

\ExplSyntaxOn

\tl_set:Nn \l_tmpa_tl { A }
\tl_set:Nn \l_tmpb_tl { B }
\tl_new:N \l_tmpc_tl
\cs_generate_variant:Nn \tl_set:Nn { Ne }

\benchmark:n { \tl_set:Nx \l_tmpc_tl { \l_tmpa_tl \l_tmpb_tl } }

\benchmark:n { \tl_set:Ne \l_tmpc_tl { \l_tmpa_tl \l_tmpb_tl } }

\stop

yields, on my machine,

3.16e-7 seconds (1.01 ops)
7.78e-7 seconds (2.39 ops)

In either case, \l_tmpc_tl is assigned AB.

Why would someone want f-expansion? Good question! Until a few months ago, there was no way to do full recursive expansion in expansion contexts. Things changed when the primitive \expanded was added to all engines (it used to be allowed only in LuaTeX), except Knuth TeX, of course.

egreg
  • 1,121,712
  • I do agree with OP that "full expansion" is rather misleading in this context. Would it not be better to change that to "expansion up to first obstacle" and claim that the f stands for "first"? – schtandard Jun 05 '19 at 09:42
  • @schtandard Possibly. – egreg Jun 05 '19 at 09:51
  • @schtandard I thought about that several times. To defend the “full”, I'd say that with “first”, one might understand that it acts as o, i.e., does only one expansion step. So, f-expansion is full expansion (read: recursive), but unlike that made by \edef, stops at the first non-expandable token (+ gobbles it if it's a space token). f could mean “front” too. :-) – frougon Jun 05 '19 at 09:59
  • @schtandard (my comment is not really an objection to yours, because your formulation is cautious enough) – frougon Jun 05 '19 at 10:06
  • 1
    Perhaps see https://tex.stackexchange.com/q/489063 on the question of \expanded versus \romannumeral. – Joseph Wright Jun 05 '19 at 10:32
  • Thank you, @egreg for your insightful explanation. As for the last statement of your response, doesn't x do also recursive expansion, as f, but just doesn't stop when it finds typesettable content? – AlexG Jun 05 '19 at 10:33
  • @AlexG No, this recursive full expansion only stops at unexpandable tokens, including those that are made such by \noexpand or \unexpanded (in expl3 lingo \exp_not:N and \exp_not:n or variants thereof). – egreg Jun 05 '19 at 10:36
  • I was more focused on the term "recursive", @egreg. Is x also recursive? I think it is, or am I wrong? – AlexG Jun 05 '19 at 10:41
  • @AlexG (replying to this) Besides, in expansion-only contexts, a macro using an x argument type won't work as expected, because it is like trying to expand an \edef. You need to execute \edef for it to produce the desired effect. For instance, with \edef\a{\b} where \b has been defined as \def\b{\edef\c{foobar}} \b is expanded when \a's \edef is executed, but this won't define \c, because \edef doesn't do its work when expanded but when executed. (see next comment) – frougon Jun 05 '19 at 10:46
  • @AlexG Yes: upon finding an unexpandable token, the next token is expanded (if possible) and the process is restarted from the first token in the expansion. – egreg Jun 05 '19 at 10:46
  • @AlexG In contrast to x args, f args do work in expansion-only contexts. – frougon Jun 05 '19 at 10:46
  • @frougon Thanks! If I understand you correctly, in an f-type argument, \c would be defined finally, but not in an x argument. – AlexG Jun 05 '19 at 12:33
  • @AlexG Difficult to answer without the full code. If you pass \b as an x-type argument, first expansion step will give \edef\c{foobar}, then \edef will stay as is (it's unexpandable), then TeX will try to expand \c. If it survives this and this doesn't swallow the rest, the replacement text (here foobar) will be expanded too. For foobar, it doesn't change anything with normal catcodes, but if the \c macro had been more complex, doing so would completely denaturate its replacement text. Bad. On the other hand, if \b is passed as an f-type argument, the expansion stops... – frougon Jun 05 '19 at 13:54
  • @AlexG ... at the very beginning since \edefis unexpandable. This preserves the replacement text of the \c macro. It may be defined correctly if the result (still \edef\c{foobar}) is then used in such a way that the \edef reaches TeX's stomach. – frougon Jun 05 '19 at 13:56
  • @frougon Oh my, sounds difficult. But I think I got the point. More or less. Thank you! – AlexG Jun 05 '19 at 14:04
  • @AlexG Maybe easier to understand: if you call the function \iow_now:cx, one of the first things done is that LaTeX3 will do x-expansion on the 2nd argument. Therefore if you pass something like \seq_use:Nx ... in this second argument, this will fail because to properly process the x argument of \seq_use:Nx ..., you must not be in expansion-only context (there is a hidden \edef that needs to be executed for the x-type argument). On the other hand, \seq_use:Nf ... would process the argument normally, even inside the 2nd arg of \iow_now:cx, because an f-type argument... – frougon Jun 05 '19 at 14:06
  • @AlexG ... works in expansion-only contexts. Yes, this is tricky and difficult to explain in a few words. If it's still unclear, this is probably good for a new question. – frougon Jun 05 '19 at 14:08
  • @frougon Thus, the today's finding (for myself) is: Never nest expl3 functions with x-type argument. – AlexG Jun 05 '19 at 14:12
  • 1
    @AlexG Better than that: functions marked with a red star in interface3.pdf (aka, “fully expandable functions”) are safe to use inside an x-type argument (but those with a hollow star are not good to use inside an f-type argument). This is what the bottom of page 4 of interface3.pdf says. – frougon Jun 05 '19 at 14:14
  • Regarding your last paragraph starting with "Why would someone want f-expansion? Good question!" Does this paragraph mean to say that f-expansion should be considered deprecated now that all engines support the \expanded primitive, or does it mean to say that one would use f-expansion for the same reasons one would use the \expanded primitive? – Evan Aad Jan 01 '23 at 10:20
  • 2
    @EvanAad Nowadays, usages of f-expansion are waning out; but there are cases where it might come handy if one doesn't really want all-the-way-expansion. – egreg Jan 01 '23 at 10:40
8

Compare is with x-expansion:

\documentclass{article}
\usepackage{expl3}

\begin{document}
\ExplSyntaxOn
\tl_set:Nn \l_mya_tl { A }
\tl_set:Nn \l_myb_tl { B }
\tl_set:Nf \l_myc_tl { \l_mya_tl STOP \l_myb_tl }
\tl_show:N \l_myc_tl 

\tl_set:Nx \l_myc_tl { \l_mya_tl STOP \l_myb_tl }
\tl_show:N \l_myc_tl

\ExplSyntaxOff\end{document}

This will give

> \l_myc_tl=ASTOP\l_myb_tl .
<recently read> }

l.207 \tl_show:N \l_myc_tl

? 
> \l_myc_tl=ASTOPB.
<recently read> }

l.210 \tl_show:N \l_myc_tl
Ulrike Fischer
  • 327,261
  • Thank you, @Ulrike. Thus, \show is the wanted tool to display the content of a control sequence. How could \l_mya_tl look like such that expansion continues? – AlexG Jun 05 '19 at 09:31
  • 1
    @AlexG One easy case is if the result of recursively expanding \l_mya_tl is empty. You can also use macros inside that don't “produce” any unexpandable content but still have desirable side effects, such as gobbling specific things further in the token list (cf. macros such as \use_none:nnn). – frougon Jun 05 '19 at 09:50
7

As pointed out in the other answers and comments, f-expansion is implemented using \romannumeral which was sometimes needed in expansion contexts before the \expanded primitive was available. This answer also mentions two use cases where it might still be of use, namely expansion without a known end point and lookaheads of the next unexpandable token.

Additionally, I'd like to point out a common use case where it's even wrong to use, as it gives undesired results. This is based on the fact that, while x-expansion continues fully expanding tokens beyond the first unexpanable token, f-expansion is more eager in the case \exp_not:n is used in the token stream.

If we look at the following examples, we see that expansion is the same when \exp:not:N (\noexpand) is used:

\cs_set:Npn \foo { [FOO] }

\tl_set:Nx \l_tmpb_tl { \exp_not:N \foo bar }
\tl_show:N \l_tmpb_tl

\tl_set:Nf \l_tmpb_tl { \exp_not:N \foo bar }
\tl_show:N \l_tmpb_tl

outputs

> \l_tmpb_tl=\foo bar.

> \l_tmpb_tl=\foo bar.

On the other hand, using \exp_not:n (\unexpanded) gives different results:

\tl_set:Nx \l_tmpb_tl { \exp_not:n { \foo } bar }
\tl_show:N \l_tmpb_tl

\tl_set:Nf \l_tmpb_tl { \exp_not:n { \foo } bar }
\tl_show:N \l_tmpb_tl

outputs

> \l_tmpb_tl=\foo bar.

> \l_tmpb_tl=[FOO]bar.

This is especially important when dealing with parts of the contents of token list variables via the \tl_head:, \tl_tail:, \tl_range: etc. functions. All those wrap their result in \exp_not:n. f-expansion may seem appropriate here, but it's actually not:

\tl_set:Nn \l_tmpa_tl { \foo bar }
\tl_set:Nx \l_tmpb_tl { \tl_head:V \l_tmpa_tl }
\tl_show:N \l_tmpb_tl

\tl_set:Nf \l_tmpb_tl { \tl_head:V \l_tmpa_tl }
\tl_show:N \l_tmpb_tl

outputs

> \l_tmpb_tl=\foo .

> \l_tmpb_tl=[FOO].

As pointed out by Phelype Oleinik, protected macros behave differently as well:

\cs_new_protected:Npn \protected_foo { \foo }

\tl_set:Nx \l_tmpb_tl { \protected_foo bar }
\tl_show:N \l_tmpb_tl

\tl_set:Nf \l_tmpb_tl { \protected_foo bar }
\tl_show:N \l_tmpb_tl

outputs

> \l_tmpb_tl=\protected_foo bar.

> \l_tmpb_tl=[FOO]bar.
siracusa
  • 13,411
  • f expansion is reserved for TeX connoisseurs, it seems. Especially the last result, the verbatim foo is counter-intuitive. Could it be used as a reverse operation to \csname ... \endcsname (getting a command sequence's name)? – AlexG Jun 06 '19 at 07:08
  • 1
    Note that the expansion of \foo was foo too. I changed it to [FOO] to avoid confusion. So it's not resulting in the macro name. The point is that it's expanding \foo even though it is hidden in \unexpanded. – siracusa Jun 06 '19 at 07:46
  • Ok, thank you. It is perhaps beyond the question's scope, but would it be difficult to translate the last f-type expansion example into TeX? (To see what happens behind the scene.) – AlexG Jun 06 '19 at 08:06
  • @AlexG There are a lot of low-level expl3 macros involved here, rewriting all them to TeX is no fun. But you can see what's going on in more detail when you add \tracingmacros=1\relax before those lines. Each macro expansion is then written to the log file along with its current parameter values. In essence it's \edef\l_tmpa_tl{\unexpanded{\foo}bar} \show\l_tmpa_tl (x-expansion) vs. \expandafter\def\expandafter\l_tmpa_tl\expandafter{\romannumeral-0\unexpanded{\foo}bar} \show\l_tmpa_tl (f-expansion). – siracusa Jun 06 '19 at 08:28
  • Ah I see. This infamous romannumeral trick is involved. Thank you! – AlexG Jun 06 '19 at 10:25
  • @AlexG, siracusa (I can't ping both) It's worth mentioning that f-expansion expands \protected macros as well (or better, it expands anything that can expand). However I have to say that the problems pointed are a weird edge case :-) Usually, when writing an expandable macro, the goal is to use the f-expansion in a controlled environment (that is, no arbitrary user input), then at the end, stop the f-expansion and return the output, possibly with \unexpanded, if necessary. In such environment it's rather uncommon (and possibly wrong) to have a stray \noexpand or \unexpanded... – Phelype Oleinik Jun 06 '19 at 11:56
  • @PhelypeOleinik Thanks for the note about protected macros, I added it to the answer. As mentioned, the token list manipulation functions are largely expanable and wrap their results in \unexpanded. If you need to deal with one of them in an expandable context (not sure how often that is needed in expl3), it seems quite tricky to me to get the expansion right. For non-expandable contexts my inital assumption that f-expansion is somehow "purer" and thus preferred over full expansion was simply wrong. – siracusa Jun 07 '19 at 02:36