6

I need to give specific name to a macro based on a condition.

The first example works, the second does not.

Is using \csname/\endcsname (i.e., example 1) the only right way? Are there any other ways to make example 2 work?

\count255=0
\def\newdef{\advance\count255 by1
 \expandafter\def\csname
  \ifnum\count255=1
    foo%
  \else
    bar%
  \fi
 \endcsname
}
\newdef{hello}
\newdef{world}
\foo\ \bar
\bye
\count255=0
\def\newdef{\advance\count255 by1
  \ifnum\count255=1
    \def\foo
  \else
    \def\bar
  \fi
}
\newdef{hello}
\newdef{world}
\foo\ \bar
\bye
8

In your second example, when the \ifnum takes the first branch, TeX executes \def\foo\else..., which is quite problematic as we'll explain below. Things are similar when the other branch is taken: \def\bar\fi....

What happens precisely

Let's take for instance the case where the \ifnum test is true. In this case, \foo gets defined, but not the way you expected. The actual definition is:

\def\foo\else\def\bar\fi{hello}

This is a valid macro definition where the parameter text is \else\def\bar\fi, and the replacement text is hello. So, whenever \foo is expanded after this definition, TeX wants to see the tokens \else\def\bar\fi right afterwards. But in your example, the token after \foo's call is a control space \, therefore you get the error:

./faulty.tex:11: Use of \foo doesn't match its definition.
l.11 \foo\ 
           \bar
Runaway argument?

The other case defines \bar as:

\def\bar\fi{world}

which, when expanded towards the end of the TeX run, fails for the same reason (\bye isn't the same token as the expected \fi).

Remedy

A simple way to fix that is to expand the \else or \fi at the appropriate time, in order to remove the unwanted tokens from the input stream before TeX gets to execute the \def (i.e., before the \def from the chosen branch reaches TeX's stomach):

\count255=0
\def\newdef{\advance\count255 by1
  \ifnum\count255=1
    \expandafter\def\expandafter\foo
  \else
    \expandafter\def\expandafter\bar
  \fi
}
\newdef{hello}
\newdef{world}
\foo\ \bar
\bye

This is essentially the same trick as the classical one using LaTeX's \@firstoftwo and \@secondoftwo.

Expanding the \else when the condition is true removes all tokens between the \else and the matching \fi (both inclusive; note that if the condition were still undecided, a frozen \relax would be inserted). Expanding the \fi simply removes it. So, in the first case, what is left in the input stream is:

\def\foo{hello}
\newdef{world}
\foo\ \bar
\bye

and in the second case:

\def\bar{world}
\foo\ \bar
\bye

Going a little further

I said above that if you try to expand the \else token when the condition is still undecided, a frozen \relax is inserted. In this case, making this become reality only requires adding a percent sign after \ifnum\count255=1, because then TeX would continue expanding tokens after the 1 (the 〈number〉 following the = sign wouldn't be finished yet) and would therefore expand the two \expandafters before this 〈number〉 has been fully read. So, removing the end part of the code that doesn't matter here, the test code could be:

\count255=0
\def\newdef{\advance\count255 by1
  \ifnum\count255=1% bug here: the second <number> isn't finished!
    \expandafter\def\expandafter\foo
  \else
    \expandafter\def\expandafter\bar
  \fi
}
\newdef{hello}\show\foo
\bye

which shows the inserted frozen \relax inside \foo's definition:

> \foo=macro:
\relax \else \expandafter \def \expandafter \bar \fi ->hello.
l.9 \newdef{hello}\show\foo
  • I added an explanation of precisely what happens in your second example. See also Going a little further about the frozen \relax in case you were to put a percent sign in the wrong place (where it would prevent a 〈number〉 from being finished). – frougon Jun 26 '19 at 7:18
6

The problem is, as described in Frougon's very fine answer, that conditionals in TeX work in quite a peculiar way: when the test is evaluated true, \else...\fi will only disappear as a result of expanding \else, which removes everything up to the matching \fi (without expansion). When the test is evaluated false, everything up to \else (or \fi) will be removed (without expansion). A dangling \fi will disappear anyway, because its expansion is void.

The method with \csname works, because expansion is performed all the way until only character tokens remain (other unexpandable tokens will throw a Missing \endcsname error), so \else and \fi will disappear as described above.

Workaround? Since you're doing definitions, expandability is not a concern and you can use Knuth's preferred construction with tail recursion and \next.

\count255=0
\def\newdef{\advance\count255 by1
  \ifnum\count255=1
    \def\next{\def\foo}%
  \else
    \def\next{\def\bar}%
  \fi
  \next
}
\newdef{hello}
\newdef{world}
\foo\ \bar

\bye

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.