(誤字脱字を発見しているのでこっそり直します.ついでにこっそりと推敲も・・・)
はじめに
TeX言語を扱う際に感じる違和感やよく出くわす意味不明な挙動は,一般的な言語の挙動からの類推と一致しないことに由来することが多いですが,TeX言語は基本的に「展開」(置き換え)の連鎖であるというところに根本的な要因があると考えられます.つまり,TeX言語が「置換」を主な動作とする,まさに「マクロ」であることを念頭において考えると,謎な挙動も腑に落ちることになります(それが分かりやすいかは全く別のお話ですが).
特に「なんでだっ!」と思ってしまうことが多いのが条件分岐です.条件分岐の詳細は「TeX by Topic」(Victor Eijkhout, Addison-Wesley, 1992,邦訳,富樫秀明,「TeX by Topic」アスキー,1996)の13.7,13.8節に詳しいのですが,綺麗にまとまりすぎていて,逆にすっきりしないので,とても泥臭く,例を加えて書き下してみます.
なお,「TeX by Topic」の原書はhttps://eijkhout.net/texbytopic/texbytopic.html にあるようにGnu Free Documentation Licenseで公開されており,TeX Liveであればtexdoc texbytopic
で参照できますし,PDF版は今でも更新が行われており,https://github.com/VictorEijkhout/tex-by-topic/ でgitのレポジトリとして公開されていますので,最新版も自由に読むことができます.なお,texdocのものよりもレポジトリの方が新しいです.
条件分岐
さて,TeX言語に限らず,何かしようとすると必ず必要になるのはいわゆる制御構造です.しかし,TeX言語には一般的なfor
のような「繰返し」はありません.ですが「条件分岐」と「再帰」はあります.したがって「繰返し」は可能です.よってTeX言語は関数型言語です(すごい結論).
ということで,まずは「条件分岐」を考えましょう.条件をチェックして,その真偽に応じた処理をするというところは同じなので,他の言語と同じ感覚で処理できる場合もあるのですが,状況によっては大変に困惑する事態に遭遇します.
さて,ここで恣意的ですが
\documentclass{jarticle}
\begin{document}
\ifodd1 \textbf \else \textsf \fi {ABC}
\end{document}
というのを考えます.\ifodd
は次にくる数字が奇数なら真となる条件分岐ですので,これを処理すると\textbf{ABC}
になりそうですが,これは
! Too many }'s.
\textbf #1->\ifmmode \nfss@text {\bfseries #1}
\else \hmode@bgroup \text@com...
l.7 \ifodd1 \textbf \else
\textsf \fi {ABC}
というエラーを出します(エラーの行数は\ifodd
のある行).しかも,そのエラーが意味不明です.そもそも{
と}
の対応は{ABC}
しかないので間違えていません.しかしエラーといわれたので,このエラーを追いかけていくことにします1.
条件分岐の展開
TeX言語は「展開」です.その意味で「条件分岐の展開」は次のようになっています.なお,TeX言語での\if
系命令は,\if
や\ifodd
などのように調べる対象ごとに複数存在します.それを一つ一つ書くのは大変なので単に\if
だけでそれらを総称します.決して「\if
」だけのことではありません.
\if ... \fi
(\else
がない)で条件が真の場合
条件のチェックのあとに,...
の展開を行います.展開をすすめていき,\fi
が現れればそこで条件分岐が終わったとみなされます.ただしこの終端の\fi
は明示的に与えられていなくても...
の展開の結果で現れたものでも構いません.
\ifodd1 A \fi %問題なくAが現れる
\ifodd1 A %処理は続行され文書全体の処理の終了時に`\fi` がないと警告がでる
\def\A{A\fi}
\ifodd1 \A %`\A`の中の`\fi`が現れ,条件分岐が終端される.
注意が必要なのは,\if
で条件が真であったときに条件分岐の終端の\fi
の扱いです.
「真」と判定された際にそれに対応する\fi
は(存在していれば)実はそのまま残されます.TeXは先頭から順番に展開していくだけです.感覚的には「真」と判定されたら,条件分岐の構造に関係するものは消えてしまう,つまり,
\ifodd1 A \fi
-
\ifddd1
が真 -
A
だけが残る
と考えてしまいますが,実際には
\ifodd1 A \fi
-
\ifddd1
が真 -
A\fi
が残る - Aをそのまま残す(条件分岐の最中だと認識されている)
-
\fi
を見つけて条件分岐が終了
という流れです.以下の例をみてみます.
\def\X#1#2#3{#2#3{#1}}
\ifodd1 AB\X{CD}\fi\textbf{EF}
条件が真になり展開が進みます.AB
はそのままですが,\X
の展開の際に\X
の引数は順番にCD
(#1
)と\fi
(#2
)と\textbf
(#3
)となります.\fi
は条件判断の際に一緒に消えるということはなく,そのまま残っているのです.そして\X
が展開されると,順番が変えられて(#2#3{#1}
つまり``\fi\textbf{CD}`となって)
AB\fi\textbf{CD}{EF}
と展開されるので,AB
のあとの\fi
で条件分岐が終わます.\fi
は展開の結果で現れたものでも構わないというのはこういうことを意味します.そして,CD
は\textbf
の引数となり,{EF}
はそのまま残るだけです(もし\textbf
が引数を二つとるものであるならば{EF}
も引数になり,トリックのタネになりえます).
これを踏まえると,上の例とほぼ同じですが,
\def\X#1#2{\fi#2{#1}}
\ifodd1 AB\X{CD}\textbf{EF}
のように,\fi
そのものを隠ぺいし,順番を変えたりもできます.
\if ... \fi
(\else
がない)で条件が偽の場合
これはシンプルです....
の部分に他の条件分岐が入れ子になっていればその構造も考慮して\fi
も含めて一気に読み飛ばします.したがって,例えば
\def\X#1#2{\fi#2{#1}}
\ifodd0 AB\X{CD}\textbf{EF} % `\fi`がないというエラー
\ifodd0 AB\X{CD}\fi\textbf{EF} % `\textbf{EF}`になる
となります.\X
は展開もされずただ読み飛ばされ(繰り返しますが,この際には入れ子の条件分岐は考慮されます),したがって仮に\X
の展開結果に\fi
があっても関係なく,読み飛ばしの境界として\fi
が必要であることに注意が必要です.
\if ... \else ... \fi
で条件が真の場合
条件が真なので,最初の...
が展開されます.その結果\else
に到達する,もしくは展開で\else
が現れると,\else
を含めて\fi
までが読み飛ばされます2.
次の例を考えます.
\def\X#1#2#3{#2#3{#1}}
\ifodd1 AB\X{CD}\else\textit\fi{EF}
条件が真なので,AB\X{CD}\else\textit ...
が展開されます.\X
は順番にCD
,\else
,\textit
を引数にとり,\else\textit{CD}
に展開され,その結果,\else\textit{CD}\textit\fi{EF}
が得られます.そして\else
から\fi
が読み飛ばされてAB{EF}
になります.なお,\else
は展開の結果で現れるもので構わず,\else
を隠ぺいすることは可能ではあります.
\if ... \else ... \fi
で条件が偽の場合
ほとんどこれまでと同様です.条件が偽なので,条件分岐の入れ子を考慮しつつ\else
まで読み飛ばし,実質的に... \fi
だけが展開されます.
\def\X#1#2#3{#3#2{#1}}
\ifodd0 AB\X\else \X{CD}\textit\fi{EF}
を考えると,条件が偽なので,\else
までは条件分岐の入れ子は考慮されますが,展開はされずに読み飛ばされます.したがって,AB\X
の\X
は引数に何かをとることもなく,そのままなくなります.そして,\X{CD}\textit\fi
が残りますが,\X
の引数としてCD
,\textit
,\fi
がとられ,\fi\textit{CD}{EF}
となり,\fi
で条件分岐が終端されます.
最初のエラーの分析
最初のエラー
\ifodd1 \textbf \else \textsf \fi {ABC}
! Too many }'s.
\textbf #1->\ifmmode \nfss@text {\bfseries #1}
\else \hmode@bgroup \text@com...
l.7 \ifodd1 \textbf \else
\textsf \fi {ABC}
に戻ります.\textbf
などの\textXX
の形のマクロは滅茶苦茶にざっくりいうと,数式中にあれば,\nfss@text
(\nfss@text
の実体は\mbox
です)3の中で書体を変えて文字を出力して,そうでなければイタリック補正を考慮して書体を変えて文字を出力するというものです4.
さて,\ifodd1
が真なので,\textbf
が展開されます.このとき,\else
が\textbf
の引数となってしまいます.そうすると,\textbf
の展開で
\ifmmode \nfss@text { \bfseries \else }
というものが現れます.ここで\ifmmode
がでてきますが今は数式モードではありませんので,条件が偽なので\else
まで読み飛ばします.よって,\else
の直後の}
が最初に現れることになります.つまり,! Too many }'s.
になるのです.
\if
はあくまでも展開と読み飛ばしを行うことを強調しておきます.先ほどのエラーの説明では
\ifmmode \nfss@text { \bfseries \else }
が現れ,また\nfss@text
は実質的には\mbox
です.それならば,\bfseries \else
は\mbox
の引数となってさらに\mbox
の処理に進むのではないかと思いがちですが5,いまは\ifmmode
は偽なので読み飛ばしを行っており,したがって,\else
が残って上述のような状況になります.{
と}
の中に\else
があるので紛らわしいのですが,条件分岐による読み飛ばしは{
と}
を単にそこにあるもの(トークン)としてしかみず,その意味に関係なく読み飛ばすのです.
TeX言語の条件分岐は以上のような形で展開されます.「条件の真偽が確定したあと(展開せずに)読み飛ばす」という動作と「先頭から展開(置き換え)をする」というマクロの動きが組み合わさっているということを念頭に置く必要があるのです.
これを考えると,\textXX
を展開する前に\else
なり\fi
なりをみつけて,不要な部分を先に読み飛ばすことができればよさそうだということになります.\textXX
を飛び越えればよいので,答えは\expandafter
です.
\ifodd1\expandafter\textbf
\else\expandafter\textit
\fi{ABC}
このコードの動作が普通に期待されるものでしょう.条件分岐と\expandafter
のこのパターンの組み合わせはLaTeX2eのカーネルでも使われています.
読み飛ばしと\if
と\fi
の訳の分からない話
\iffalse\let\ifnum=\iftrue\fi
これはエラーになります.\iffalse
なので\fi
まで読み飛ばされそうですが,ここで留意しなければいけないことは「読み飛ばしの際には条件分岐の構造を考慮する」ということです.しかも,読み飛ばしの際には展開はされません.\let\ifnum=\iftrue
はTeXの文法としては正しいので,なんとなくこれが実行されて読み飛ばされて外側の\iffalse .. \fi
が完結するように思いがちですが,そもそも実行されないのです.実行されないところにきて,\if
の構造だけはチェックされるので,\fi
が足りないとみなされます.つまり,
\iffalse\let\ifnum=\iftrue\fi\fi\fi
と「内側」の\ifnum
と\iftrue
に対する\fi
をそれぞれ追加しないといけません.そのうえ,そもそも実行されないので,実は
\iffalse\ifnum=\iftrue\fi\fi\fi
という文法的に意味のわからないものでも\fi
が要求されます.
さらに,
\iffalse\let\ifHOGE=\iftrue\fi\fi\fi
のように新たに\ifHOGE
を定義するようなものを入れてみると,今度は三つの\fi
では多すぎるというエラーになります.これは\ifHOGE
は見た目は\if
なのですが,未定義なので\if
ではありません.読み飛ばしの際の\if
の構造のチェックは見た目ではなく,それが内容的に\if
であるかが使われるのです.したがって,この場合は
\iffalse\let\ifHOGE=\iftrue\fi\fi
と\fi
を二つにしないとエラーになります.これらを踏まえると
\let\HOGE=\ifnum
\iffalse\HOGE=\iftrue\fi\fi\fi
\let\EGOH\fi
\iffalse\HOGE=\iftrue\fi\EGOH\fi
はエラーになりません.\HOGE
は\ifnum
で,\EGOH
は\fi
なのです.なお,このタイプのトリック(見た目は(たしかに\if
でも\fi
でもないけど実はそうなっている)は\loop ... \repeat
で使われています.\let\repeat\fi
ですが,実質的に使われてないので,削除します)しかし,
\def\HOGE{\ifnum}
\iffalse\HOGE=\iftrue\fi\fi\fi
はエラーです.\HOGE
は展開されれば\ifnum
になりますが,読み飛ばしでは展開はされません.したがって\fi
は二つにする必要があります.
これと同様のケースがTeX by Topicの13.8節にあります.
\iffalse\csname iftrue\endcsname\fi\fi
このときは\fi
が多いというエラーがでます.\iffalse
と\iftrue
なので,本物の\if
が二つで\fi
も二つに見えます.しかし,「展開はされない」ので\csname
もそこにあるだけです.つまり内側は\iftrue
ではないので,最初の\iffalse
に対応するのは一つ目の\fi
です.よって,二つ目の\fi
が余計なのです.
また,「読み飛ばしでは展開されない・(条件分岐の構造以外での)意味は問わない」ということは以下の四つはTeX的に正しいものです(エラーを出しません).
\iftrue{\else}\fi
{\ifnum0=`}\fi
\iffalse{\else}\fi
\ifnum0=`{\fi}
これらは正しいといわれても訳が分かりません.上の二つは{
だけが,下の二つは}
が現れます.しかも,それぞれで{
と}
対応が正しいので「使える」形になっています.つまり,条件分岐とグループは独立していることになります.この{
と}
を作り出すトリックは表組(LaTeXではtabular環境やarray環境,eqnarray環境)で使われています6.
条件の確定と展開
今までの例で\ifodd
の直後の数字の後ろにはわざと空白をいれていました.さて,この空白は何でしょうか? 以下のような場合を見てみます
\def\ONEodd{1odd}%
\ifodd2\ONE\else even\fi
\ifodd2
なのでこの条件は偽で,\else
まで読み飛ばされて「even」となるように見えますが,実は違います.「odd」になります.
条件分岐の説明はあくまでも「条件の真偽が確定したあと」のことです.しかし,条件が確定する段階に注意が必要なことがあります.
\ifodd
のように条件の確定に何らかの値が必要なものは,その値が確定するまで先のほうまで展開をしようとします.そこで\ifodd2
のように明示的に空白をいれておくと,この空白が「数字の終端」を明示することになり,\ifodd
の対象の値を確定させるためにそれ以上展開することはありません.したがって,
\ifodd1 \textbf \else\textit\fi
の場合の\textbf
の展開は,\ifodd1
で条件が真と確定され,そのあとに真の場合の\textbf
が展開されているという状況です.一方,
\def\ONEodd{1odd}%
\ifodd2\ONEodd\else even\fi
の場合は,\ifodd
の対象の数としてまず2
がありますが,まだ続きがあるかわからないため\ONEodd
を展開します.その結果,\ifodd21odd
となり,o
は数字ではないため,ここが数の終端を表し,21
が\ifodd
の対象となり,真偽が確定します.したがって,「odd」が現れる展開は条件が確定する前に行われています.この場合\ONEodd
の展開のトリガになるものは\ifodd
であり,真偽が確定したあとの展開とは展開のタイミングが異なるのです.
一般にTeXには「数が期待される箇所では数が確定するまで展開する」という仕様7があり,この仕様によって「いつの間にか行われる展開」は常に意識が必要です.
さて,条件分岐の説明で「\fi
は展開の結果で現れたものでもよい」という言及をしました.そのときに少し触れましたが.展開の結果で現れる\else
についてみてみます.
\def\AAA{\else X}
\def\BBB{{\else X}}
\def\CCC{1\else X}
\ifodd2 \AAA\fi%%(1)
\ifodd2\AAA\fi %%(2)
\ifodd2\BBB\fi %%(3)
\ifodd2\CCC\fi %%(4)
(1)の場合は2
の後ろの空白で\ifodd
は2
だけを対象にすることがわかるので,\AAA
は読み飛ばされます.仮に\AAA
が引数を求めるマクロであっても,読み飛ばされるだけです.
(2)の場合,\ifodd
の対象が確定しないので\AAA
が展開されます.その結果\iffodd2\else X\fi
となって,\else
は数字ではないので,\ifodd2
が偽になって,X
が出力されます.
(3)の場合,\ifodd2{\else X}\fi
になります.2
の直後に{
({
と}
は展開できません)があるので条件は偽になり,{
が読み飛ばされてX}\fi
になります.結果X}
が残るのですが,}
が適切に処理されなければToo many }'s
のエラーになります.
(4)の場合は,\ifodd21\else X\fi
となるので結果は何も現れません.
このように条件の確定のための展開で現れた\else
も条件分岐の処理に影響を与えます.しかしこれは条件が確定する前に現れたので.「展開の結果現れた\fi
,\else
でもよい」という言及とは前提が異なりますことには注意してください.
ついでに,条件が確定したあとの展開で現れる\else
も考えてみます.
\def\A{TRUE\else FALSE}
\iftrue\A\else\fi %%(5)
\iffalse\A\else\fi %%(6)
(5)の場合は,条件が真なので,TRUE\else FALSE\else\fi
となり,\A
の展開で現れた\else
から\fi
までが読み飛ばされます.\else
から\fi
の間には\fi
は許されませんが,\else
はチェックされず,結局は「TRUE」が現れます.
(6)の場合ですが,条件が偽で確定しているので\else
までは読み飛ばされ,そもそも展開されません.つまり,\else
が展開で現れることはありません.
LaTeX2eでの条件分岐の例(\@ifundefined
)
LaTeX2eでは\if ... \else ... \fi
を隠ぺいした条件分岐のマクロがいくつか定義されています.それらの中で条件分岐の展開がメインであるものに\@ifundefined
があります.
\@ifundefined{<name>}
{\<name>が未定義のときの動作}
{\<name>が定義済みのときの動作}
もっともシンプルな(古典的な)ケース
\def\@ifundefined#1{%
\expandafter\ifx\csname#1\endcsname\relax
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi}
eTeX以前のTeXがベースのLaTeX2eの定義がこれです.使用例からは引数が三つに見えますが,定義の上では引数が一つで\@ifundefined{<name>}
とすると\<name>
というコントールシーケンスを\csname ...\endcsname
で作って8,それが\relax
であるかをチェックします.一方,\csname ... \endcsname
で作られたコントールシーケンスは,それが未定義であった場合は\relax
として扱われます.したがって,\relax
と比較されることで未定義がどうかを判別していることになります.また,このことからLaTeX2eでは\relax
と同じものは未定義とみなされることが多いということになります.
ここで条件の真偽が定まったあと,偽の場合は\expandafter\@secondoftwo\fi
,真の場合は\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
の展開に入りますが,ここでも\expandafter
があるので\@secondoftwo
,\@firstofone
は一度飛ばされ,\fi
か\else
の展開に入ります.\fi
のときはそのまま読み飛ばされ,\else
のときは\fi
まで一気に読み飛ばされます.したがって,\@ifundefined{<name>}
は,\<name>
が未定義の場合は\@firstoftwo
,定義済の場合は\@secondoftwo
に展開されます.これらは引数を二つ取り,名前の通り二つのうちの「一つ目」「二つ目」に展開されます.よって,\@ifundefined
は期待通りの処理を行います.
さて,\HOGE
が未定義のときに,普通に\HOGE
を使おうとすると,当然undefinedのエラーとなります.ところが,\HOGE
と「ほぼ」同じである\csname HOGE\endcsname
は\HOGE
が未定義であるので,\relax
扱いになりエラーとなりません.しかも,\HOGE
が未定義のときに一度でも\csname HOGE\endcsname
を使うと,その後は\HOGE
そのものも\relax
となってまうという副作用があります.ただしこの「暗黙の\relax
化」の副作用はそのスコープ内でのことです.従って,それ以降では\HOGE
はもうエラーにはなりません.この副作用は,\@ifundefined
で未定義か否かのチェックをした際にも発生します.つまり,チェックによってその対象の状態が変わってしまうのです.ただこれも\relax
と未定義は同じと見なすという規約の下では大きな問題ではないかもしれません.しかし,未定義なら必ずエラーになってほしいというのは否定できません.これを解決するのがeTeX拡張\ifcsname ...\endcsname
です.
eTeX拡張を使った現在の定義
\def\@ifundefined#1{%
\ifcsname#1\endcsname\@ifundefin@d@i\else\@ifundefin@d@ii\fi{#1}}
\long\def\@ifundefin@d@i#1\fi#2{\fi
\expandafter\ifx\csname #2\endcsname\relax
\@ifundefin@d@ii
\fi
\@secondoftwo}
\long\def\@ifundefin@d@ii\fi#1#2#3{\fi #2}
見た目,かなり複雑になりました.展開で現れる\fi
のほかにマクロの引数で\else
や\fi
を取り払うことも行われています.
\ifcsname ...\endcsname
の使い方はシンプルです.\csname ...\endcsname
の代わりにすれば, それが未定義であれば偽,定義済であれば真になるというシンプルなものです.また\csname ...\endcsname
のような副作用はありません.しかしもちろん\csname ...endcsname
そのものの副作用がなくなったわけではないことはいうまでもありません.
さて,シンプルな「未定義の場合」から考えます.注意が必要なのは\@ifundefined{<name>}{未定義の場合}{定義済の場合}
というマクロの構文は当然同じですが,\ifcsname ...\endcsname
は定義済が真,未定義が偽であるという自然な形であることです.つまり,「未定義の場合」は\ifcsname ...\endcsname
にとっては偽のケースです.\@ifundefined
の定義の該当する部分を抜き出します.
\@ifundefin@d@ii\fi{#1}{未定義の場合}{定義済の場合}
\long\def\@ifundefin@d@ii\fi#1#2#3{\fi #2}
本体は\@ifundefin@d@ii
で,引数のパターンパッチで\fi
を飲み込みます.展開を追いかけると
\@ifundefin@d@ii\fi{#1}{未定義の場合}{定義済の場合}
\fi -> \fi
#1 -> #1
#2 -> 未定義の場合
#3 -> 定義済の場合
=>
\fi 未定義の場合
=>
未定義の場合 %%\ifcsname ..endcsnameの終端の\fiが消える
となり,これは期待された動作です.
次に「定義済」の場合の動作です.
...\@ifundefin@d@i\else\@ifundefin@d@ii\fi{#1}}
\long\def\@ifundefin@d@i#1\fi#2{\fi
\expandafter\ifx\csname #2\endcsname\relax
\@ifundefin@d@ii
\fi
\@secondoftwo}
\long\def\@ifundefin@d@ii\fi#1#2#3{\fi #2}
\@ifundefin@d@i
の引数がちょっと分かりにくいかもしれません.#1\fi
の書式は条件分岐の構文の中で,マクロの引数のパターンマッチで\fi
までを読み飛ばす場合によく使われます.実際
#1\fi -> \else\@ifundefin@d@i#1\fi
#2 -> #1
という引数になります.展開の最初に\fi
があるので,ここで\ifcsname ...\endcsname
の条件分岐が終わります.そして,\@ifundefin@d@i
の定義をみると#1
は使われていません.これで読み飛ばしが実現されました.
#2
はチェック対象のコントロースシーケンスを表す文字列ですが,この段階で定義済なのはチェックされています.しかしここに\expandafter\ifx\csname #2\endcsname\relax
があります.チェック済なのにもう一度チェックがされているように思われますが,これは互換性のためのコードです.\ifcsname ...\endcsname
は実体が\relax
であっても「定義済」とみなします.「未定義と\relax
は同じではない」という立場です.しかし今までの\@ifundefined
は\relax
は未定義とみなすという動作なので,これを変えてしまうと影響が大きいと思われます.そこで,定義済かつ\relax
であるかという条件をここでチェックしています.ここで\relax
となった場合,\@ifundefin@d@ii\fi...
と処理が進みます.この後の展開は以下のようになります.
\@ifundefin@d@ii\fi\@secondoftwo{未定義の場合}{定義済の場合}
\long\def\@ifundefin@d@ii\fi#1#2#3{\fi #2}
\fi -> \fi %%パターンマッチ
#1 -> \@secondoftwo
#2 -> 未定義の場合
#3 -> 定義済の場合
=>
\fi #2 -> \fi 未定義の場合 %%\@secondoftwo が呑み込まれて消える
=>
未定義の場合 %%\ifxの条件分岐の終端で\fiが消える
これで\relax
であった場合も未定義の場合の処理になります.くどいようですが,\csname ...\endcsname
が使われてもすでに定義済なので,副作用「いつのまにか\relax
になっていた」は関係ありません.
最後に,定義済で\relax
でもないケースです.これは単純で,すでに条件分岐の中からは抜け出していて,最後に\@secondoftwo
が残っているだけで,\@secondoftwo{未定義の場合}{定義済の場合}
によって「定義済の場合」が現れて処理が終わります.
これで\@ifundefined
の中の副作用を回避してなおかつ従来の\relax
の扱いを保った\@ifundefined
ができました.
番外(LuaLaTeXの場合)
\def\@ifundefined#1{%
\ifcsname#1\endcsname
\expandafter\ifx\lastnamedcs\relax\else\@ifundefin@d@i\fi
\fi
\@firstoftwo}
\long\def\@ifundefin@d@i#1#2#3#4#5{#1#2#5}
LuaLaTeXの場合はまたちょっと違います.よくみると\lastnamedcs
というのがあります.これはLuaTeXのプリミティブでこれより前に使われた\csname ...\endcsname
(\ifcsname
などの亜種も含む)のうち,もっとも最後に使われたものに展開されるというものです.注意が必要なのは,\lastnamedcs
は,
\def\ABC{123}
\csname ABC\endcsname
\gdef\lastnamedcs{\ABC}
のように定義されるということです9.
これをふまえて,LuaLaTeX版の\@ifundefined
をおいかけます.
定義済で\relaxではない場合
\@ifundefuned{#1}{未定義の場合}{定義済の場合}
=>
\expandafter\ifx\lastnamedcs\relax\else\@ifundefin@d@i\fi
\fi
\@firstoftwo{未定義の場合}{定義済の場合}
=>
\ifx\#1\relax\else\@ifundefin@d@i\fi %%%\expandafterで\lastnamedcsが先に展開される
\fi
\@firstoftwo{未定義の場合}{定義済の場合}
=>
\@ifundefin@d@i\fi %%%偽なので\elseまで読み飛ばし
\fi
\@firstoftwo{未定義の場合}{定義済の場合}
=>
\@ifundefin@d@i#1#2#3#4#5 -> #1#2#5
#1 -> \fi %%\ifxの終端の\fiを飲み込む
#2 -> \fi %%\ifcsname ...\endcsnameの終端の\fiを飲み込む
#3 -> \@firstoftwo
#4 -> 未定義の場合
#5 -> 定義済の場合
=>
\fi %% \ifxの終端の\fiになる
\fi %% \ifcsname ...\endcsnameの終端の\fiになる
定義済の場合
=>
定義済の場合
定義済で\relaxの場合
...
=>
\ifx\#1\relax\else\@ifundefin@d@i\fi %%%\expandafterで\lastnamedcsが先に展開される
\fi
\@firstoftwo{未定義の場合}{定義済の場合}
=>
\fi %%%真なので\fiまで読み飛ばされ\ifcsname ...\endcsnameの\fiに到達
\@firstoftwo{未定義の場合}{定義済の場合}
=>
\@firstoftwo{未定義の場合}{定義済の場合} %%\fiが消える
=>
未定義の場合
未定義の場合
\@ifundefuned{#1}{未定義の場合}{定義済の場合}
=>
\@firstoftwo{未定義の場合}{定義済の場合} %%\ifcsname ..\endcsnameを一気に読み飛ばす
=>
未定義の場合
LuaLaTeXの場合も同じ動作の\@ifundefined
が定義できました.副作用のない\ifcsname ...\endcsname
も重要ですし,三つの定義のどれもが,条件分岐と展開のみで実装されている,いわゆる「完全展開可能」なマクロであることも重要です.
また,LuaLaTeXの\lastnamedcs
によって,テストの対象(\@ifundefined
の第一引数)が保存されるので引数のトリックが減って,実装がシンプルになっているのが面白いです10.
おわりに
TeX by TopicをベースにTeX言語の条件分岐を考えてみました.
TeX言語の条件分岐は直観的ではないケースがままあります.しかし,条件分岐は,例えば再帰構造を実現するのに必須の要素であり,実際にLaTeX2eでは再帰構造を使って何種類かの繰り返し構造が実現されています.
したがって,まずは再帰とかはおいておくとしても,条件分岐を理解するのはいろいろな応用の基礎的な部分になり重要だと思います.
-
本質的に同じでもっとシンプルな例はありますが,実際にみかけたことのあるサンプルとしてこれをとりあげます. ↩
-
この読み飛ばされる部分に
\else
が単独にあるのは問題ありませんが,条件分岐の構文上のバランスを壊す単独の\if
や\fi
はエラーとなります. ↩ -
\mbox
なので,a_{\textbf{x}}
の下付き添え字のx
は本文サイズのx
のボールド体です.個人的には書体変更の\textXX
を数式で使うのはお勧めしません. ↩ -
実際には
\DeclareRobustCommand
によって定義されているので,展開に関する巧妙な細工が施されていますが,話を簡略化するためここでは触れません. ↩ -
実際は
\mbox
もマクロなのでもうちょっと処理がありますが,そこは省略します. ↩ -
TeXの入れ子を数えるカウンタの動作と関係して複雑で本稿の趣旨とあわないので,
\if
が使われる厄介な例としてだけ挙げておきます. ↩ -
この仕様を逆手に取った「
\romannumeral
トリック」と呼ばれる展開制御の手法が存在します. ↩ -
このように,
\csname
に処理を移すのが典型的な\expandafter
の使い方です.\expandafter
がないと,\ifx
は\csname
を次のものと比較しようとしますが,これは明らかに意図とは違います.\expandafter
によって\ifx
が飛び越えられて,\csname
が展開され,\csname <name>\encsname
が\<name>
になります. ↩ -
lastnamedcsはlast named csでしょうか. ↩
-
LuaTeXを作っている人たちは
\csname ...\endcsname
に不満があったに違いないです(笑).よさげなプリミティブがいくつか追加されていて,LuaTeXのマニュアルをみてて,思わず「あー,これ分かるわぁ」という気分になりました. ↩