Some other points, which I think is not mentioned or only briefly mentioned in other answers.
Only fully-expandable functions can conveniently "return result" as argument to another functions.
Imagine you have a function \increaseone{5}
that when executed typeset 6
.
Now imagine you want to call \dosomethingwith{\increaseone{5}}
and you want it to be the same as \dosomethingwith{6}
. This is only possible(*) when \increaseone
is fully expandable (and \dosomethingwith
fully expands its argument).
If that's not the case, you'd usually need to store them into temporary "variables" manually e.g.
\increaseone[store-to=\tmp]{5}
\dosomethingwith{\tmp}
Consequently, for programming convenience(†), people tend to try to make functions that "return results" expandable (⋆-type functions in expl3), if it doesn't make the performance significantly worse (when it does, they try to make it ✩-type expandable at least).
(*) Technically it's also possible for the outer function and the inner function to "agree" on some "intermediate" location to pass the result i.e. the inner function stores the result there, then the outer function reads the result from it. See functional
package for an example of that approach.
(†) and only for programming convenience. Everything you can do with fully expandable macros, you can make an unexpandable macro set an intermediate "variable" (or use a callback, etc.) like the method above. Unless you're using some library that provides an "API" that only accepts fully expandable functions, that is.
(some other case where expandability is more convenient)
Consider the tabular
environment for example.
\begin{tabular}{|c|c|}
\int_step_inline:nn {5} {
#1 & #1 \\
}
\end{tabular}
This code will print an extra cell, like this. (assume you set the correct catcode)
The reason is that in the low-level tabular
is implemented in terms of halign
which uses some complicated logic to determine whether or not there is a cell, and if there's something unexpandable it determines there is.
For example the following code produces the same output
\begin{tabular}{|c|c|}
1 & 1 \\
2 & 2 \\
3 & 3 \\
4 & 4 \\
5 & 5 \\
\relax
\end{tabular}
Reference: Completely expandable loop macro that also works in tabular
Note that, again, expandability is not required, just more convenient. Using the expandable \int_step_function:nN
would fix this, but the code can also be rewritten without expandability like this
\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}
It's the reader's exercise to figure out how to eliminate the added asymptotic complexity.
Changes in LuaTeX.
First, there's the new primitive \immediateassignment
, which eliminates all the slowness caused by expansion-only programming as mentioned above.
Then, there's \directlua
which is expandable, and allow you to use Lua to store it things. (although both Lua and TeX uses hash table, using Lua hash table has the advantage of not needing to have a "common unique prefix/suffix" for each table, thus speed up the hashing/retrieval. And more convenient programming, of course. + it's faster to handle unbalanced token list in Lua, as you need intermediate representation in TeX + TeX string hashing function has some issues, see above)
There are a few things that can only be done unexpandably. (more examples.)
Parsing optional argument is mentioned above as one. You can't distinguish \function {[} something ]
and \function [ something ]
expandably. (to be fair, if you assume that the following token is either a {
or a [
then you can stringify it, but the general question remains)
Some others worth mentioning is distinguishing between some other-catcode character (e.g. !
) from its active-catcode version that is \let
to its other-catcode. (e.g. \let <active token !> = <other token !>
other than defining macro that use the fork pattern for each 256 characters
(because expansion-only mode is a peculiarity in TeX, such limitations are also peculiarities in TeX. See LuaTeX below.)
Generate a character token with given charcode/catcode combination. again, other than defining macro for each of the 256 characters, or use some XeTeX/LuaTeX primitive.
Anything that involves typesetting. You can't "get the width of the character a
" without setting a box, which is unexpandable.
(side note unrelated to this question, there are some things cannot be done unexpandably as well. For example
- check if the following token already has catcode frozen (this one as far as I know still cannot be done in LuaTeX)
- implement
peek_analysis_map_inline
function completely reliably (this one can be done in LuaTeX)
for example with a program like this
%! 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
will mistake that the following token is \c
.
)
Expansion-only programming is slower. (more details.)
This is because in expansion-only programming, the memory model is somewhat similar to a stack machine(‡) (there's only one input stream for you to push data to and pop data from. So you have to "skip" through the "front" of the "stack" to access the data below, and that is slow)
For instance, it's possible to reverse all items in a token list (which consists of all braced groups) in time O(n) using toks registers (or use control sequences indexed with numbers with some performance caveat), but to the best of my knowledge, it's only possible (without LuaTeX) to do that in O(n√n) expandably, or O(n log n) assuming \expanded
is available. The expl3 implementation of \tl_reverse_items:n
has time complexity O(n²).
(‡) Technically, there are a few other tricks to store data in other places as well e.g. when you do \number 12345\<some expandable things>
the 12345
is "stored" before the expandable things, or when you do \expandafter \somecontrolsequence \<some expandable things>
, and you can pack a detokenized token list into a control sequence etc.. Nevertheless most of the time this is not possible.
Even with \expanded
included, you have a "stack" and a "another stack of places to push tokens to", nevertheless the memory model is still limited and not as powerful as tok registers.
What "fully expandable" means. (clarification.)
This only "makes sense" for "functions" that are expected to "return result".
For example if a function is supposed to typeset something or set some global variable, it isn't really necessary(§) for it to be fully expandable, as there's no desired "result" to get from its full expansion. As such it's usually protected.
However, functions that compute some value e.g. addition etc. should usually be expandable. If it somehow cannot be implemented expandably, usually there's a function to store it to some "variable" to be retrieved later.
conditional/mapping functions is usually the difficult part here. For expandable conditional, you expect (pseudocode) \edef \a { \ifsomething 1 \else 2 \fi }
to assign either 1
or 2
to "variable" \a
, but this only holds when \ifsomething
is expandable.
Otherwise you need \ifsomething \edef \a {1} \else \edef \a {2} \fi
(again, pseudocode).
(§) apart from LuaTeX this cannot be implement expandably anyway.
Side note.
Some side remark on the memory model of expansion-only programming (briefly mentioned above, section 4 "Expansion-only programming is slower"), TeX slowness, and how the engine could be made to interpret TeX faster.
First, we know that when something like this happens...
\def \a #1 {\b {#1}}
\def \b #1 {\c {#1}}
\def \c #1 {\d {#1}}
each function call requires re-scanning, which takes time linear in the number of tokens in #1
.
So, currently it's not possible to implement modifiable random-access memory in O(1) or O(log N) time per operation
(ignoring the possibility of setting control sequences to \relax
or expl3 "flags")
In theory it's possible for TeX to keep track of the }
matches each {
to quickly speed-up to that point
(and the dangling }
on the input stream, if any), but there are a few complications...
- In order to save memory, TeX token lists are keep track internally as a linked list, and the same
⟨replacement text⟩
might be reused multiple times in multiple places. So the same }
might be matched to multiple different {
tokens.
- In the progress of scanning, it's necessary to check if there's any
\outer
token in the braced group. As such, every time some token changes its \outer
status (hopefully rare), the whole input stream (the part that is tokenized at least) needs to be rescanned to determine whether there's an \outer
token inside a braced group.
- Similar for tokens that is produced after a
\noexpand
. Argument-scanning should revert them to normal tokens.
- Similar for
\par
. (although this one might be easier because it's matched by token instead of by meaning? Not sure.)
- For brace hacks, the implementation needs to be careful to correctly match the
{
within the reinserted tokens. (this part may not be very hard, as the list of dangling }
are kept)
Other than that, if there's some TeX implementation that makes argument scanning O(1) in non-corner cases,
something like a random-access memory could be implemented in expandable TeX programming with only O(log N) overhead (where N is the number of "memory cells")
by maintaining e.g. a balanced binary tree.
Such an implementation would probably speed up existing code, although I'm not sure by how much because much of the existing code are optimized with the assumption that argument scanning takes O(N); besides there's a constant-time overhead of needing to keep track of brace balancing..
I'm not sure if it's possible to make argument scanning O(1) in all cases or not, but it seems possible without the complication of \outer
and \noexpand
tokens, at least.
Side note, I believe that if instead of reusing the same memory cell over and over (back then, memory was very limited), they're actually copied,
the time complexity would remain the same(*) but TeX would run faster because of memory locality. Of course, there would exist some corner case code that takes exponentially more memory than the old implementation.
(*): I think so with the current implementation. If the implementation uses smart argument grabbing, some careful consideration is needed.