Help us understand the problem. What is going on with this article?

TeX言語の条件分岐

More than 1 year has passed since last update.

(誤字脱字を発見しているのでこっそり直します.ついでにこっそりと推敲も・・・)

はじめに

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://bitbucket.org/VictorEijkhout/tex-by-topic/src/default/ で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は先頭から順番に展開していくだけです.感覚的には「真」と判定されたら,条件分岐の構造に関係するものは消えてしまう,つまり,

  1. \ifodd1 A \fi
  2. \ifddd1が真
  3. Aだけが残る

と考えてしまいますが,実際には

  1. \ifodd1 A \fi
  2. \ifddd1が真
  3. A\fiが残る
  4. (条件分岐の最中だと認識されている)
  5. \fiを見つけて条件分岐が終了

という流れです.以下の例をみてみます.

\def\X#1#2#3{#2#3{#1}}
\ifodd1 AB\X{CD}\fi\textbf{EF}

条件が真になり展開が進みます.ABはそのままですが,\Xの展開の際に\Xの引数は順番にCD\fi\textbfになります.\fiは条件判断の際に一緒に消えるということはなく,そのまま残っているのです.そして\Xが展開されると,順番が変えられて

AB\fi\textbf{CD}{EF}

になるので,ABのあとの\fiで条件分岐が終わます.\fiは展開の結果で現れたものでも構わないというのはこういうことを意味します.そして,CD\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の処理に進むのではないかと思いがちですが,\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が余計なのです.

また,前にも触れましたが,「読み飛ばしでは展開されない・(条件分岐の構造以外での)意味は問わない」ということは

\iftrue{\else}\fi
{\ifnum0=`}\fi

\iffalse{\else}\fi
\ifnum0=`{\fi}

は,これまた訳が分からないですが,ともに正しい構文です.上の二つは{だけが,下の二つは}が現れます.しかも,{}対応が正しいので「使える」形になっています.つまり,条件分岐とグループは独立していることになります.この{}を作り出すトリックは表組(LaTeXではtabular環境やarray環境,eqnarray環境)で使われています.

条件の確定と展開

今までの例で\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には「数が期待される箇所では数が確定するまで展開する」という仕様5があり,この仕様によって「いつの間にか行われる展開」は常に意識が必要です.

さて,条件分岐の説明で「\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の後ろの空白で\ifodd2だけを対象にすることがわかるので,\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で作って6,それが\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}

のように定義されるということです7

これをふまえて,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の第一引数)が保存されるので引数のトリックが減って,実装がシンプルになっているのが面白いです8

おわりに

TeX by TopicをベースにTeX言語の条件分岐を考えてみました.

TeX言語の条件分岐は直観的ではないケースがままあります.しかし,条件分岐は,例えば再帰構造を実現するのに必須の要素であり,実際にLaTeX2eでは再帰構造を使って何種類かの繰り返し構造が実現されています.

したがって,まずは再帰とかはおいておくとしても,条件分岐を理解するのはいろいろな応用の基礎的な部分になり重要だと思います.


  1. 本質的に同じでもっとシンプルな例はありますが,実際にみかけたことのあるサンプルとしてこれをとりあげます. 

  2. この読み飛ばされる部分に\elseが単独にあるのは問題ありませんが,条件分岐の構文上のバランスを壊す単独の\if\fiはエラーとなります. 

  3. \mboxなので,a_{\textbf{x}}の下付き添え字のxは本文サイズのxのボールド体です.個人的には書体変更の\textXXを数式で使うのはお勧めしません. 

  4. 実際には\DeclareRobustCommandによって定義されているので,展開に関する巧妙な細工が施されていますが,話を簡略化するためここでは触れません. 

  5. この仕様を逆手に取った「\romannumeralトリック」と呼ばれる展開制御の手法が存在します. 

  6. このように,\csnameに処理を移すのが典型的な\expandafterの使い方です.\expandafterがないと,\ifx\csnameを次のものと比較しようとしますが,これは明らかに意図とは違います.\expandafterによって\ifxが飛び越えられて,\csnameが展開され,\csname <name>\encsname\<name>になります. 

  7. lastnamedcsはlast named csでしょうか. 

  8. LuaTeXを作っている人たちは\csname ...\endcsnameに不満があったに違いないです(笑).よさげなプリミティブがいくつか追加されていて,LuaTeXのマニュアルをみてて,思わず「あー,これ分かるわぁ」という気分になりました. 

kbhonda
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away