TeX & LaTeX Advent Calendar
2015/12/01 〜 2015/12/25〜今さら人に聞けない、TeXのキホン〜
☃ TeX & LaTeX アドベントカレンダー 2015 ☃
某カレンダー(↑)の 16 日目はコレだった。
- \mathchoice の闇 (TeX Alchemist Online)
ヤバい \text 命令
つまり、現在の amstext パッケージの \text
命令の実装では、(数式モードの時に)引数のテキストが 4 回実行される(ただし実際に出力に現れるのはそのうちの 1 回だけ)ことになり、これが原因で直感に反する挙動が起こることがある。カウンタ値の増加についてはパッケージで“大概うまくいく対策”が取られているが、この“対策”の想定から外れるケースでは“もっと不可解な挙動”になってしまう恐れがある。
だからヤバくない \text 命令を作りたかったのに
「4 回実行」をやっている限りはこの問題が完全に解決できる見込みはない。何とかして「引数のテキストは 1 回だけ実行される」実装ができないものだろうか。
素朴に考えると次のような考えが思い浮かぶであろう。
それ自身は何も出力しない \mathchoice
を使って、「現在の数式スタイルが(D、T、S、SS*1のうちの)どれか」を判定してその結果を“変数”(マクロなど)に保存する。そのあとに、その変数の値による条件分岐を用いて*2「擬似 mathchoice」を行う。
一見簡単な処理のようにみえるが、実はこの「“変数”に保存する」というのが極めて難しい。
%(数式モード中) \mathchoice % \@mstyle に"代入"する {\def\@mstyle{1}}% D {\def\@mstyle{2}}% T {\def\@mstyle{3}}% S {\def\@mstyle{4}}% SS \ifcase\@mstyle\relax % 実際に分岐する \or D \or T \or S \or SS \fi
元ネタの記事で説明があるように、\mathchoice
はサブ数式を構成するので、そこでは代入が局所化された状態(\begingroup〜\endgroup
の中と同じ)になる。だから上のコードでは、\mathchoice
の実行後には \@mstyle
は未定義に戻っている。
局所化されているなら、大域代入を使えばどうか。
\mathchoice % \@mstyle に"代入"する {\gdef\@mstyle{1}}% D {\gdef\@mstyle{2}}% T {\gdef\@mstyle{3}}% S {\gdef\@mstyle{4}}% SS
これもダメである。なぜなら、\mathchoice
の選択肢は「4 つとも(順に)実行される」からで、\@mstyle
の値は必ず「4」になってしまう。そもそも「4 つとも実行して」いる以上、\mathchoice
の実際の選択に関する情報はどうやっても外に伝わらない。
実は、TeX の数式組版処理の性質上、「現在の数式スタイルが何か」は“その場では”決まらず、数式全体を見終わった後で初めて決まるものとなっている。\mathchoice
が「4 つとも組版しておいて、後で正しいのを選ぶ」という奇妙な仕様を有しているのも、そもそも“後”にならないとどれが正しいかが判断できないからなのである。
それでもヤバくない \text 命令を作りたい
極めて絶望的な状況であるが、実は“抜け道”がある。「2 パス処理」を持ち込めば実現できるのである。
「現在の数式スタイルがどれか」を判定してその結果を補助ファイル(.aux)に保存する。その後に、前回コンパイル時の判定結果の値を用いて、条件分岐で「擬似 mathchoice」を行う。
ここでのポイントは、補助ファイルへの書込を遅延書込(\immediate
でない \write
)にすることである*3。遅延書込はページを出力する時になって初めて実行されるものなので、「その場では数式スタイルは決まらない」という原則的な制限の影響を受けない。そして実際に、\mathchoice
の選択肢の中で遅延書込を行った場合は、その選択肢が実際に採用(出力)された場合にのみ書込が行われる。結果的に、補助ファイルには「数式スタイルが何か」に関する正しい情報が書き込まれることになる。
そしてヤバくない \text 命令を作った
「2 パス処理」といえば、もちろん zref パッケージの出番である。そういうわけで、実際に zref パッケージを利用して、作ってみた。
- bxamstext パッケージ (Gist/zr-tex8r)
実装についての解説は後日に措いて、ここでは使い方だけ説明しておく。基本的に、amstext パッケージの代わりにこの bxamstext パッケージを読み込めばよい。
\usepackage{bxamstext}
これで amstext の全ての機能が使える。内部で amstext を読み込んでその実装を修正する、という手順を踏んでいるので、amstext を内部で読み込む他のパッケージ(amsmath 等)とも共存できる。
実際の例をみてみよう。
% XeLaTeX/LuaLaTeX 文書; 文字コードは UTF-8 \documentclass[a4paper]{article} \usepackage{fontspec} \newfontfamily\fIpxm{IPAexMincho} \usepackage{bxamstext} %\usepackage{amstext} \newcommand\☃{\text{{\fIpxm ☃}% \typeout{Snowman's here!}}}% ノイズ \begin{document} We have: $S = \Bigl\{\☃^{\☃^{\☃}}\Bigr\}$. \end{document}
プレアンブルで定義されている \☃
命令は数式中で ☃ を(安全に)出力するためのものであるが、挙動の違いを見るために、わざと \text
の引数に \typeout
を入れている。もし amstext パッケージを読み込んでコンパイルした場合は、1 回で正しい結果が得られるが、
Snowman's here!
のメッセージ表示が \☃
1 つにつき 4 回(文書中に \☃
は 3 つあるので計 12 回)行われることになる。
これに対して、bxamstext を読み込んでコンパイルした場合は、メッセージ表示は \☃
1 つにつき 1 回(全体で 3 回)しか行われないが、次の警告メッセージが出る。
LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.
出力をみると、全ての☃が同じサイズになっている。
文書をもう 1 度コンパイルすると警告が消えて、正常な出力が得られる。