読者です 読者をやめる 読者になる 読者になる

TeX Alchemist Online

TeX を使って化学のお仕事をしています。

\mathchoice の闇

TeX TikZ

この記事は TeX & LaTeX Advent Calendar 2015 の16日目の記事です。 15日目はokomokさんでした。 17日目はあざらしさんです。

本記事では,LaTeX ユーザでも知っておくと役立つ TeX のプリミティブ \mathchoice の解説を行い,その使用上の注意点を取り上げます。

ちなみに,この記事中の画像は全て拙作の TeX2img で作成しています(ステマ)

目次

\mathchoice とは?

\mathchoice は,使用場所に応じて数式の出力のされ方を変えるために用いられるプリミティブです。4つの引数を伴って使います。

\mathchoice{ディスプレイ}{テキスト}{スクリプト}{スクリプトスクリプト}
  • 第1引数(ディスプレイ)は,\[ \]amsmathalign 環境などのディスプレイ数式モードで出力されるときの出力スタイルを定めます。
  • 第2引数(テキスト)は,$ $ で出力されるインライン数式モードで出力されるときの出力スタイルを定めます。
  • 第3引数(スクリプト)は,添字位置で出力されるときの出力スタイルを定めます。
  • 第4引数(スクリプトスクリプト)は,ani のような「添字の添字」の位置で出力されるときの出力スタイルを定めます。

典型的な使用例

例えば,LaTeX 標準の \frac は,4つのスタイルに応じて次のような出力になります。

\[\frac{1}{2} = {\textstyle\frac{1}{2}} 
= x^{\frac{1}{2}} = y^{z^{\frac{1}{2}}}\]

f:id:doraTeX:20151216153305p:plain

これを,仮に次のようにカスタマイズしたいとしましょう。

  • ディスプレイスタイルの分数は,もう少し線を長めにして,分母と分子を線に少し近づけたい。
  • テキストスタイルの分数は,もう少し線を長めにしたい。
  • スクリプトやスクリプトスクリプトの分数は,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}}}\]

f:id:doraTeX:20151216153458p:plain

確かに出力スタイルが場所に応じて変化していることが分かります。便利ですね!

使用上の注意点

このように,\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\]

をコンパイルするとどうなるでしょうか。

結果はこうなります。

f:id:doraTeX:20151216163611p:plain

\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 ずつ増えていきそうな気がするでしょう。しかし,実際にコンパイルしてみると,

f:id:doraTeX:20151216164646p:plain

となり,\mycount の値が予想以上に激増していることが分かります。

これは,\mathchoice の4つの引数が全部実行されていることによるものです。次の図を見ると分かりやすいでしょう。四角で囲んだ部分が,実際に出力されたものになります。

f:id:doraTeX:20151216165814p:plain

そして,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}\]

f:id:doraTeX:20151216164646p:plain

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}\]

すると,

f:id:doraTeX:20151216222056p:plain

という出力になり,確かに出力スタイルに応じて出力内容が分岐されていることが分かります。

では,

\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 という出力になると予想されます。

f:id:doraTeX:20151216181718p:plain

ところが,実際にコンパイルしてみると,1, 2, 3, 4 という素直な出力になります。

f:id:doraTeX:20151216181309p:plain

なぜこのような出力になるのかは,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 に包んで使用

それぞれの出力結果は以下の通りとなります。

f:id:doraTeX:20151216183035p:plain

このように,一見理解不能な出力になってしまいます。

これは,amstext.sty では,\stepcounter\addtocounter\iffirstchoice@ を考慮するよう再定義されているが,\setcounter は再定義されていないことに起因しています。

つまり,\text{\five} を実行すると,

  1. ディスプレイ用実行:\setcounter{mycounter}{3}\addtocounter{mycounter}{2} が両方実行され,\arabic{mycounter} は 5 を出力。
  2. テキスト用実行:\setcounter{mycounter}{3} は実行されるが,その後の \addtocounter{mycounter}{2} は空振りし,\arabic{mycounter} は 3 を出力。
  3. スクリプト用実行:2. と同様。
  4. スクリプトスクリプト用実行: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$

というコードのコンパイル結果が

f:id:doraTeX:20151216184811p:plain

になるということも理解できるでしょう。

なお,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

とすれば直ちに確認できます。

この問題は,北川さんが 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 を作り直すことでこの問題を抜本的に解決しようという策を提案してくださいました。

d.hatena.ne.jp

d.hatena.ne.jp

bxamstext パッケージをロードすると,(2パス処理が必要になってはしまいますが)\mathchoice の4つの引数のうち該当する1個しか実行されないようになります。 これで安心して \text できますね!