この記事は TeX & LaTeX Advent Calendar 2015 の16日目の記事です。 15日目はokomokさんでした。 17日目はあざらしさんです。
本記事では,LaTeX ユーザでも知っておくと役立つ TeX のプリミティブ \mathchoice
の解説を行い,その使用上の注意点を取り上げます。
ちなみに,この記事中の画像は全て拙作の TeX2img で作成しています(ステマ)。
目次
\mathchoice
とは?
\mathchoice
は,使用場所に応じて数式の出力のされ方を変えるために用いられるプリミティブです。4つの引数を伴って使います。
\mathchoice{ディスプレイ}{テキスト}{スクリプト}{スクリプトスクリプト}
- 第1引数(ディスプレイ)は,
\[ \]
やamsmath
のalign
環境などのディスプレイ数式モードで出力されるときの出力スタイルを定めます。 - 第2引数(テキスト)は,
$ $
で出力されるインライン数式モードで出力されるときの出力スタイルを定めます。 - 第3引数(スクリプト)は,添字位置で出力されるときの出力スタイルを定めます。
- 第4引数(スクリプトスクリプト)は,ani のような「添字の添字」の位置で出力されるときの出力スタイルを定めます。
典型的な使用例
例えば,LaTeX 標準の \frac
は,4つのスタイルに応じて次のような出力になります。
\[\frac{1}{2} = {\textstyle\frac{1}{2}} = x^{\frac{1}{2}} = y^{z^{\frac{1}{2}}}\]
これを,仮に次のようにカスタマイズしたいとしましょう。
- ディスプレイスタイルの分数は,もう少し線を長めにして,分母と分子を線に少し近づけたい。
- テキストスタイルの分数は,もう少し線を長めにしたい。
- スクリプトやスクリプトスクリプトの分数は,1/2 のようなスラッシュで出力したい。
これらの希望を実現する \myfrac
という命令を定義してみます。
\makeatletter \def\@myfrac@d#1#2{\displaystyle\frac{\raisebox{-.44ex}{$\,#1\,$}}{\raisebox{.1ex}{$\,#2\,$}}} \def\@myfrac@t#1#2{\textstyle\frac{\hspace{.05em}#1\hspace{.05em}}{\hspace{.05em}#2\hspace{.05em}}} \def\@myfrac@s#1#2{\scriptstyle#1/#2} \def\@myfrac@ss#1#2{\scriptscriptstyle#1/#2} \def\myfrac#1#2{\mathchoice{\@myfrac@d{#1}{#2}}{\@myfrac@t{#1}{#2}}{\@myfrac@s{#1}{#2}}{\@myfrac@ss{#1}{#2}}} \makeatother
使ってみましょう。
\[\myfrac{1}{2} = {\textstyle\myfrac{1}{2}} = x^{\myfrac{1}{2}} = y^{z^{\myfrac{1}{2}}}\]
確かに出力スタイルが場所に応じて変化していることが分かります。便利ですね!
使用上の注意点
このように,\mathchoice
は一般の LaTeX ユーザが利用しても便利なプリミティブなのですが,これには次のような癖の強い仕様があります。
現在のスタイルにマッチするものだけが組版されるのではなく,4つのスタイル全てが一度組版された後,実際に必要なスタイルだけが出力される。
確かめてみましょう。
$\mathchoice{\typeout{D}}{\typeout{T}}{\typeout{S}}{\typeout{SS}}$
をコンパイルすることを考えます。 直観的には「Tだけ出力される」ように見えますが,実際にコンパイルしてみると,コンソールには
D T S SS
と出力され,確かに4つ全てが実行されていることが分かります。
このことをしっかり意識しておかないと,カウンタ演算などを含む命令を \mathchoice
に入れたときに「期待と異なる出力になる」ことになりかねませんので,注意が必要です。
カウンタ演算と \mathchoice
先程定義した \myfrac
を例にとって,カウンタ演算と \mathchoice
を混ぜて使う場合の挙動を確かめておきましょう。
TeX 式カウンタの場合
局所化されたカウンタ演算の場合
\newcount\mycount \def\hogeA{\advance\mycount\@ne\the\mycount}
\hogeA
は「現状の \mycount
の値に 1 を加えてその値を出力する」という命令です。
では,
\mycount=0 \[\myfrac{\hogeA}{\hogeA} = {\textstyle\myfrac{\hogeA}{\hogeA}} = x^{\myfrac{\hogeA}{\hogeA}} = y^{z^{\myfrac{\hogeA}{\hogeA}}} = \the\mycount\]
をコンパイルするとどうなるでしょうか。
結果はこうなります。
\advance
によるカウンタ演算は局所化されているので,ボックス定義内部での演算の影響は外に及びません。ディスプレイスタイルとテキストスタイルでの分母・分子はボックスに包まれているので,ボックスを出ると \mycount
の値が 0 にリセットされる結果,分母・分子の値はともに 1 として出力されています。
\myfrac
におけるスクリプトスタイル・スクリプトスクリプトスタイルの定義では #1/#2
となっており,#1 と #2 をそれぞれグルーピングしていないため,\hogeA
を使うごとに値が増加しています。
また,\mathchoice
全体が局所化されており,\mathchoice
を出ると \mycount
の値が 0 に戻っていることも分かります。
\global
を付けたカウンタ演算の場合
次に,\advance
に \global
を付けて
\newcount\mycount \def\hogeB{\global\advance\mycount\@ne\the\mycount}
と定義しましょう。
\mycount=0 \[\myfrac{\hogeB}{\hogeB} = {\textstyle\myfrac{\hogeB}{\hogeB}} = x^{\myfrac{\hogeB}{\hogeB}} = y^{z^{\myfrac{\hogeB}{\hogeB}}} = \the\mycount\]
のコンパイル結果はどうなるでしょうか。直観的には,\hogeB
を使うたびに \mycount
の値が 1 ずつ増えてゆくから,\myfrac{\hogeB}{\hogeB}
を1回使うたびに \mycount
が 2 ずつ増えていきそうな気がするでしょう。しかし,実際にコンパイルしてみると,
となり,\mycount
の値が予想以上に激増していることが分かります。
これは,\mathchoice
の4つの引数が全部実行されていることによるものです。次の図を見ると分かりやすいでしょう。四角で囲んだ部分が,実際に出力されたものになります。
そして,4回の \myfrac
呼び出しが終わった後の \mycount
の値は 32 となっています。
LaTeX 式カウンタの場合
LaTeX 式カウンタに対する \setcounter
や \stepcounter
は,定義に \global
が付いているので,上記の \global
付きの場合と同様の結果になります。
\newcounter{mycounter} \def\hogeC{\stepcounter{mycounter}\arabic{mycounter}} \setcounter{mycounter}{0} \[\myfrac{\hogeC}{\hogeC} = {\textstyle\myfrac{\hogeC}{\hogeC}} = x^{\myfrac{\hogeC}{\hogeC}} = y^{z^{\myfrac{\hogeC}{\hogeC}}} = \arabic{mycounter}\]
amsmath の \text
の挙動 (1)
amsmath パッケージ(から呼び出される amstext.sty
)によって定義される \text
は,内部的に \mathchoice
が使われており,それゆえ,使用箇所によって出力形式が異なります。
典型的な例として,「現在のフォントサイズを出力する」命令を作成し,それを \text
の中で使ってみましょう。
まずは現在のフォントサイズを出力する命令 \currentFontSize
を定義します。
\def\currentFontSize{% \expandafter\expandafter\expandafter\@currentFontSize \expandafter\detokenize\expandafter{\the\font}\relax} \def\@currentFontSize#1/#2/#3/#4/#5 \relax{#5}
次にそれを \text
の中で使ってみます。
\def\hogeD{\text{\currentFontSize}} \[\hogeD, {\textstyle\hogeD}, {\scriptstyle\hogeD}, {\scriptscriptstyle\hogeD}\]
すると,
という出力になり,確かに出力スタイルに応じて出力内容が分岐されていることが分かります。
では,
\newcounter{mycounter} \def\hogeE{\text{\stepcounter{mycounter}\arabic{mycounter}}} \setcounter{mycounter}{0} \[\hogeE, {\textstyle\hogeE}, {\scriptstyle\hogeE}, {\scriptscriptstyle\hogeE}\]
をコンパイルするとどうなるでしょうか。
先程の \hogeC
の例からすると,\hogeE
を使うたびにカウンタが余分に回り,1, 6, 11, 16 という出力になると予想されます。
ところが,実際にコンパイルしてみると,1, 2, 3, 4 という素直な出力になります。
なぜこのような出力になるのかは,amstext.sty
を読んでみると分かります。
まず,\text
は次のように定義されています。
\DeclareRobustCommand{\text}{% \ifmmode\expandafter\text@\else\expandafter\mbox\fi} \let\nfss@text\text \def\text@#1{{\mathchoice {\textdef@\displaystyle\f@size{#1}}% {\textdef@\textstyle\f@size{\firstchoice@false #1}}% {\textdef@\textstyle\sf@size{\firstchoice@false #1}}% {\textdef@\textstyle \ssf@size{\firstchoice@false #1}}% \check@mathfonts }% }
そして,\iffirstchoice@
の真偽に応じて挙動が変わるよう,\stepcounter
と \addtocounter
が再定義されています。
\newif\iffirstchoice@ \firstchoice@true \def\stepcounter#1{% \iffirstchoice@ \addtocounter{#1}\@ne \begingroup \let\@elt\@stpelt \csname cl@#1\endcsname \endgroup \fi } \def\addtocounter#1#2{% \iffirstchoice@ \@ifundefined {c@#1}{\@nocounterr {#1}}% {\global \advance \csname c@#1\endcsname #2\relax}% \fi}
つまり,\stepcounter
と \addtocounter
は,\text
から呼び出される \mathchoice
の第1引数(ディスプレイ)でしか動かず,第2引数以降では無効化されるようになっているのです。それゆえ,直観的に分かりやすい挙動(\text
の引数があたかも1回しか実行されていないように見える)が実現されています。
amsmath の \text
の挙動 (2)
しかし,この \text
の挙動には落とし穴があります。次の例をご覧ください。
まず,
\def\five{% \setcounter{mycounter}{3}% \addtocounter{mycounter}{2}% \arabic{mycounter}% }
と定義します。mycounter
の値を 3 にした後に 2 加えて出力するのですから,5 という出力が当然期待されます。
では,使ってみましょう。
\five % 地の文で使用 $\five^{\five^{\five}}$ % インライン数式で使用 $\text{\five}^{\text{\five}^{\text{\five}}}$ % インライン数式で \text に包んで使用 \[\text{\five}^{{\text{\five}}^{\text{\five}}}\] % ディスプレイ数式で \text に包んで使用
それぞれの出力結果は以下の通りとなります。
このように,一見理解不能な出力になってしまいます。
これは,amstext.sty
では,\stepcounter
と \addtocounter
は \iffirstchoice@
を考慮するよう再定義されているが,\setcounter
は再定義されていないことに起因しています。
つまり,\text{\five}
を実行すると,
- ディスプレイ用実行:
\setcounter{mycounter}{3}
と\addtocounter{mycounter}{2}
が両方実行され,\arabic{mycounter}
は 5 を出力。 - テキスト用実行:
\setcounter{mycounter}{3}
は実行されるが,その後の\addtocounter{mycounter}{2}
は空振りし,\arabic{mycounter}
は 3 を出力。 - スクリプト用実行:2. と同様。
- スクリプトスクリプト用実行:2. と同様。
という4回の実行がなされます。その結果,\text{\five}
はディスプレイ位置では 5 を,それ以外の位置では 3 を出力します。
この動きを理解すれば,
\newcounter{mycounter} \def\hogeF{\text{% \arabic{mycounter}% \setcounter{mycounter}{3}% \addtocounter{mycounter}{2}% \arabic{mycounter}% }} \setcounter{mycounter}{5} \hogeF, \hogeF, \hogeF \par D: $\displaystyle \hogeF, \hogeF, \hogeF$ \par T: $\textstyle \hogeF, \hogeF, \hogeF$ \par S: $\scriptstyle \hogeF, \hogeF, \hogeF$ \par SS: $\scriptscriptstyle \hogeF, \hogeF, \hogeF$
というコードのコンパイル結果が
になるということも理解できるでしょう。
なお,calc.sty
も \stepcounter
などの定義を書き換えますが,
\usepackage{amsmath} \usepackage{calc}
の順序でロードすれば,amstext.sty
での再定義に配慮して,\iffirstchoice@
を考慮した再定義がなされるようになっています。
\setcounter
の定義を修正する
amstext.sty
の定義にならって,\stepcounter
や \addtocounter
と同様の「\mathchoice
ガード」を \setcounter
に対しても施してみましょう。
\let\latex@setcounter=\setcounter \def\setcounter#1#2{\iffirstchoice@\latex@setcounter{#1}{#2}\fi}
と再定義しておきます。すると,先程の \five
の出力は全て 5,\hogeF
の出力は全て 55 になり,直観に合う結果になるでしょう。
\mathchoice
が引き起こすその他の諸問題
\mathchoice
は,カウンタ演算関連のみならず,他にも問題を引き起こす要因になります。\mathchoice
は,その性質上,「組版したが実際の出力には現れない」というパーツが生まれます。それが様々な問題を引き起こす原因となるのです。
問題1:pTeX における \mathchoice
内の和文文字問題
以前 Qiita で報告した例です。 TeX Live 2015 以下の pTeX で,
$\mathchoice{あ}{}{}{}$
というコードをコンパイルしようとすると,pTeX エンジン自体が Segmentation fault で落ちます。
コマンドラインから
$ echo "\relax$\mathchoice{あ}{}{}{}$\bye" | ptex
とすれば直ちに確認できます。
echo "\relax$\mathchoice{あ}{}{}{}$\bye" | ptex で TeX Live 2015 の pTeX に Segmentation fault を起こさせることに成功した! pic.twitter.com/ciF8QJkzYd
— Yusuke Terada (@doraTeX) September 9, 2015
この問題は,北川さんが ptex-base.ch
を改修することによって既に対応がとられています。
これにより,pTeX のバージョンは p3.6 から p3.7 になりました。(ptex_version.h, ptex-base.ch)
TeX Live の上流にも r38333, r38334 としてコミットされており,次の TeX Live 2016 のリリースにおいて修正されることになります。(最新版の W32TeX では既に修正反映済みです。)
問題2:TikZ の patterns ライブラリを用いたパターン塗りを dvipdfmx で処理した場合の問題
以前 TeX Forum で報告した例です。
\documentclass[dvipdfmx]{article} \usepackage{tikz} \usetikzlibrary{patterns} \usepackage{amsmath} \def\test{\tikz\filldraw[pattern=north east lines] (0,0) ellipse (2 and 1);} \begin{document} $\text{\test}$ \end{document}
というソースを dvipdfmx 経由でPDF化すると,
dvipdfmx:warning: Object @pgfpatternobject3 used, but not defined. Replaced by null.
という警告が出た上で,斜線パターンが抜け落ちます。\text
で囲んだり $
で囲んだりしなければ,斜線パターンは問題なく出力されます。
この問題も,やはり \text
から呼び出される \mathchoice
に起因しています。
$\text{\test}$
を実行する際には,\test
が4回実行され,そのうち2回目(テキストスタイル)の出力結果が最終出力に残ります。そのため,初めてそのパターンを使ったとき(ディスプレイスタイル)の出力結果は DVI に吐き出されずに捨て去られます。2回目以降の使用では,1回目の使用のときになされたパターン定義を参照する動きをしているので,1回目の出力結果が DVI に残らず消え去ると,2回目以降の使用でパターン定義が見つからずに,パターンが抜け落ちる結果になってしまっているのです。
この問題は,pgfsys-xetex.def
に書かれている \pgfsys@dvipdfmx@patternobj
の定義を流用して,
\def\pgfsys@dvipdfmx@patternobj#1{\pgfutil@insertatbegincurrentpagefrombox{#1}}
と定義しておくことで回避できます。
問題3:bmpsize パッケージを dvipdfmx で使用した場合の問題
これも以前 TeX Forum で報告した例です。
bmpsize パッケージとは,\usepackage{bmpsize}
とするだけで,extractbb なしでビットマップ画像のサイズを測定できるパッケージです。TeX Live 2014 以降,pTeX + dvipdfmx でも使用可能になりました。
しかし,次のような問題があります。
\documentclass{article} \usepackage[dvipdfmx]{graphicx} \usepackage{bmpsize} \usepackage{amsmath} \def\test{\includegraphics{test.png}} \begin{document} $\text{\test}$ \end{document}
というソースをコンパイルすると,画像が欠落します。
この問題も,問題2 と同様,やはり \text
から呼び出される \mathchoice
において,「最初の組版結果が DVI に残らず捨て去られる」ことに起因しています。
この問題に対する解決策は見つかっておりませんが,ビットマップ画像に対してもPDF図版と同様に「extractbb の自動起動に任せる」ということで問題ないでしょう。TeX Live 2015 以降ではデフォルトで extractbb の自動起動が許可されていることですし。つまり,現在では,上記のような潜在的な問題発生の可能性を受け入れてまで bmpsize パッケージを使う必要性は特になさそうです。
以上,\mathchoice
の使用法と,それを巡る諸問題を紹介してきました。読者の皆様方におかれましては,注意点にお気を付けて,楽しく健全な \mathchoice
ライフを送りましょう!
追記:ZRさんによる抜本的な解決策 ― bxamstext パッケージ
本記事を受けて,ZRさんが,zref パッケージ(解説1,解説2,解説3)を使って \text
を作り直すことでこの問題を抜本的に解決しようという策を提案してくださいました。
bxamstext パッケージをロードすると,(2パス処理が必要になってはしまいますが)\mathchoice
の4つの引数のうち該当する1個しか実行されないようになります。
これで安心して \text
できますね!