マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

本当は怖い if-トークンの話 (1)

TeX Forum の「OTFパッケージの noreplace オプション」のスレッドについての話。ここで話題になっているのは、TeX プログラミングの中級者を悩ませる落とし穴の一つであるである「if-トークンの罠」である。この記事ではこの問題についての解説を行う。

問題が起こる例

まずは、(スレッドで扱われている例と同様の)「未定義の可能性のある if-トークンが if 文中に含まれている」例を挙げる。

次のような状況を考える。製作中の LaTeX パッケージにおいて、一部の機能(\somecommand 命令)は e-TeX 拡張に依存することとしたが、そのパッケージ(の残りの機能)は非 e-TeX でも使用可能にしたい。そこで、パッケージ冒頭でエンジンチェックを行ってその結果をスイッチ \ifxx@engine@is@eTeX に格納し、e-TeX 依存コードをこのスイッチによる if 文の中に置いたとする。

\ifxx@engine@is@eTeX %---- e-TeX の場合のみ実行する
  % \somecommand は e-TeX でのみ定義される
  \def\somecommand#1{%
    \ifdefined#1%
       ………… % 何かの処理
    \fi
  }
\fi                  %----

ここで、\ifdefined は e-TeX 拡張プリミティブで、e-TeX でのみ定義されている if-トークンである。このコードは実際に e-TeX である場合は意図通りに動く。しかし、非 e-TeX(つまり \ifxx@engine@is@eTeX が偽)においてはエラーになる。何故だろうか。

! Too many }'s.
l.57   }

TeX は如何に「実行しないもの」を処理するか

\ifxx@engine@is@eTeX が偽である場合、TeX は「対応する \fi」までのテキストを読み飛ばすという処理をする。ところで「対応する \fi」はどうすれば特定できるか。if 文がネストしている可能性があるので、単純に最初に見つけた \fi を正解と判断するわけにはいかない。そこで、TeX は以下のような処理を行う。

読み飛ばしている途中にある if-トークンと \fi の個数を数えて、\fi の方が多くなった時点で読み飛ばしを終了する。

例として、\ifxx@engine@is@eTeX は偽だが、(何故か)\ifdefined は有効な if-トークンであったとする。この場合、読み飛ばす部分では if と fi は次のように配置されている。

\def \somecommand #1{\ifdefined #1 …… \fi① }\fi

従って、\fi② を見つけた時点で、「if が 1 個、fi が 2 個」となるので、そこで読み飛ばしが終わる(そして \fi② が \ifxx@engine@is@eTeX に対応する \fi となる)。これは意図した動作である。しかし、実際の非 e-TeX では \ifdefined は未定義(つまり if-トークンでない)であるので、if と fi の配置は次のようになる。

\def \somecommand #1{\ifdefined #1 …… \fi① }\fi

そして \fi① の時点で、「if が 0 個、fi が 1 個」となって読み飛ばしが終了し、その直後の } から実行が再開されてしまう。これが「Too many }'s」のエラーになった原因である。

「if-トークンの罠」に遭遇する他の典型的な例が、「if-トークンを if 文以外で使用する」パターンである。

例えば、\ifxx@left@hoge\ifxx@right@hoge\ifxx@balanced@hoge という 3 つのスイッチがあったとして、「\ifxx@balanced@hoge が真の時は、\ifxx@right@hoge の値を \ifxx@left@hoge と同じにする」という処理を行いたいとして、次のようなコードを書いたとする。((スイッチ \ifxx@right@hoge のもつ「値」というのは、要するに「その制御綴が \iftrue\iffalse のどちらに \let されているか」ということなので、スイッチの「値」の複写は単に \let を用いれば実現できる。))

\ifxx@balanced@hoge
  \let\ifxx@right@hoge\ifxx@left@hoge
\fi

このコードは、\ifxx@balanced@hoge が真の場合は意図通りに動くが、偽の場合は正常に動かない。例によって、読み飛ばす時の状況を考えると:

\let \ifxx@right@hoge \ifxx@left@hoge \fi

本来、\ifxx@balanced@hoge に対応すべきだったはずの \fi を読んだ後も、「if が 2 個、fi が 1 個」なので、読み飛ばしが終わらない。結局、当該ファイルの以降の内容が全て無視された後に次のエラーが出る。

! Incomplete \iffalse; all text was ignored after line 5.
<inserted text>
                \fi
l.3

「if-トークンの罠」が怖い理由

TeX プログラミングの経験がかなりある人でも、特に気を付けていないと「if-トークンの罠」に陥ることが多いようである。それは、先述の「読み飛ばしの規則」が一見自然に見えて実は非常に直感に反する要素を含んでいるからだと思われる。

  • 文脈が全く考慮されない。マクロの定義あるいはグループの中か外かという意味的なことも、ブレース({ })の対応という構文的なことも考慮されず、ただ if と fi の対応だけを見ている。
  • なおかつ、その if と fi の対応の検査も、「全ての if-トークンが if 文として使われている」という前提で行われている。
  • そもそも「読み飛ばし」が起こる場合(条件が偽)の解釈が、条件が真で実際にコードが実行される場合の解釈と食い違う。例えば、\ifxx@balanced@hoge の例では、それが真である場合は、中の if-トークンは if 文を作らないと(正しく)解釈されている。

そして、この直感に反する要素の別の作用として、実際にエラーが起こったときの動作が全く想定できないものになる。多くの場合、デタラメな実行フローが起こって、原因の箇所から離れた所でエラーが起こり、しかもそれは条件トークンに関するエラーであるとは限らない(最初の例は「Too many }'s」であった)。なお、この「エラーが解り難い」という作用は、条件トークンに対するケアレスミス(if-トークンの綴りを間違えた、\fi が足りない、等)でも発生する。

「罠」に打ち克つための方策

「if-トークンの罠」に陥らないための対策として一番重要なのは、

TeX の if-トークンは怖い」という意識を持ち続ける

ということだと思う。どういう「罠」があるかを知っているだけでも、予防効果があるだろう。

具体的な対策としては、「罠」の発生源となる「未定義かも知れない if-トークン」や「if 文以外の if-トークン」を避けるということが基本となる。といっても、無条件にこれを避けてしまうと却って判り難いコードになってしまう場合もあり、「読み飛ばされる場合の動作」を十分に考慮した上でそのような if-トークンを限定的に使った方がよい場合もある。次回は、今回挙げた例を利用して、具体的な対処法について解説する。