マクロツイーター

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

“TeX”で2次方程式の解の公式を出力する話

「“TeX”とは何を指すのか」というのは常に悩ましい問題1であるが、自分の感覚としては「“TeX”とはplain TeX(のみ)を指す」という習慣は少なくとも現代において2は「“TeX”とはLaTeX(のみ)を指す」という習慣と同類である(要はあまり妥当でない)と考えている。

となると、「LaTeXのコードは“TeXのコード”ではない」という前提において、何だったらTeXのコード”といえるだろうか。この前提の下では「plain TeXのコード」もその他のフォーマットのコードも“TeXのコード”ではなさそうである。唯一可能性があるのは「本当にTeX処理系本来の機能のみを前提にしたコード」、つまり「iniTeXTeXのINIモード3)で動くコード」ということになるだろう。

というわけで、本記事では、この「iniTeXで2次方程式の解の公式を組版して出力するコード」についてグダグダと雑に解説していくことにする。

※雑談なので前提知識を厳密には決めないが、TeX言語🤮のキホン的な知識はあった方が楽しめると思う。

使い方を説明してみる

TeX: To typeset the quadratic formula with iniTeX · GitHub

[ini-formula.tex]
\catcode`\{=1 \catcode`\}=2 \catcode`\$=3 \catcode`\^=7
\mathcode`\+="202B \mathcode`\-="2200 \mathcode`\=="303D
\hsize=77mm \vsize=22mm \scriptspace=0.5pt
\parfillskip=0pt plus 1fil \nulldelimiterspace=1.2pt
\delimiterfactor=901 \delimitershortfall=5pt
\thinmuskip=3mu \medmuskip=4mu \thickmuskip=5mu
\font\tt=cmr10 \font\st=cmr7 \tt\fam0
\font\tm=cmmi10 \font\ts=cmsy10 \font\tx=cmex10
\textfont0=\tt \scriptfont0=\st \scriptscriptfont0=\st
\textfont1=\tm \scriptfont1=\tm \scriptscriptfont1=\tm
\textfont2=\ts \scriptfont2=\ts \scriptscriptfont2=\ts
\textfont3=\tx \scriptfont3=\tx \scriptscriptfont3=\tx
\output{\shipout\vbox to\vsize{\vfill\unvbox255}}
$$x={-b\mathchar"2206\radical"270370{b^2-4ac}\over2a}$$
\end

※ツイッタァー(現𝕏)の投稿に画像として示すために改行を少なめにしているが、それ以外には特別(なるべく短くする、チョット読みにくくする🎄、等)な書き方はしていない。

このソースは「DVI出力を利用して128mm×72mmの用紙サイズで出力すること」を前提としている。元祖TeX(のINIモード)とdvipdfmxを利用してPDFに変換するには以下のコマンドを実行すればよい。

tex -ini ini-formula.tex
dvipdfmx -p "128mm,72mm" ini-formula.dvi

出力の全体

中身を説明してみる

そういうわけで、春🌸なのでこの「iniTeX用のTeXコード」をテキトーに説明していくことにする。

基本的に「plain TeXと同じ組み方の数式」を得るのが目的であるため、「TeXの初期状態」(iniTeX起動時の状態)から始めて「plain TeXでやっている設定のうち今回必要になるものだけを選んで踏襲する」という方針をとる。

1行目

\catcode`\{=1 \catcode`\}=2 \catcode`\$=3 \catcode`\^=7

{ } $ ^の各文字4カテゴリコードをplain(やLaTeX)と同様に設定している。逆に言うとTeXの初期状態ではその設定はされていない」ということであり、実際、初期状態ではほとんどの文字のカテゴリコードは12である。

ちなみに、\のカテゴリコードについては最初から0に設定されている。そうでないとそもそも\catcode自体が使えなくてカテゴリコードの設定が何もできなくなるからである。同様の理由で英字のカテゴリコードも初期状態で11になっている。他にも空白(10)、改行(5)、%(14)等も初期状態で設定されている。

2行目

\mathcode`\+="202B \mathcode`\-="2200 \mathcode`\=="303D

ここでは+ - =の3文字5について数式コード(math code)をplainと同様に設定している。数式コードは「その文字を数式中でフツーに出力した(つまり当該の文字のカテゴリコード11または12の文字トークンを実行した)ときにどのように出力すべきか」を決定する。

  • +のコード値 "202B は「二項演算子(2)として、数式ファミリ0のフォントの文字コード "2B のグリフを出力する」ことを意味する。後で行うフォント設定に従うと、ファミリ0のフォントはcmr10であり、その文字コード "2B には(ASCIIと同じく)“+”の記号が入っている。
  • -のコード値 "2200 は「二項演算子(2)として、数式ファミリ2のフォントの文字コード "00 のグリフを出力する」ことを意味する。ファミリ2のフォントはcmsy10で文字コード "00 には“−”(マイナス)の記号6が入っている。
  • =のコード値 "303D は「関係演算子(3)として、数式ファミリ0のフォントの文字コード "3D のグリフを出力する」ことを意味する。ファミリ0(cmr10)の文字コード "3D は“=”である。

なお、TeXの初期状態の数式コードの値は以下のようになっている(xxは当該文字のASCIIコード)。

  • 英字(A~Z、a~z)については"71xx、つまり「数式英字、ファミリ1の当該の文字」。
  • 数字(0~9)については"70xx、つまり「数式英字、ファミリ0の当該の文字」。
  • それ以外は"00xx、つまり「通常文字、ファミリ0の当該の文字」。

従って、英字や数字(x a 2 4等)については数式コードの設定は不要である。

3~5行目

\hsize=77mm \vsize=22mm \scriptspace=0.5pt
\parfillskip=0pt plus 1fil \nulldelimiterspace=1.2pt
\delimiterfactor=901 \delimitershortfall=5pt

各種のレイアウトパラメタの設定である。このうち最初の2つ(\hsize\vsize)はplainの設定値ではなく独自の値を設定している7

  • \hsize\vsizeは版面のサイズを表す。左側と上側のマージンはTeXの初期値のままの1インチ8なので、ここでは右と下のマージンも1インチと想定した上で、用紙サイズ(128mm×72mm)からマージンを除いたサイズ(77mm×22mm)を設定した。

残りのパラメタはplainの設定値に合わせている。

  • \scriptspaceは添字の直後に挿入される空きの大きさ。
  • \parfillskipは段落の末尾に自動的に追加されるグルーの大きさ。段落最終行を左揃えにするため普通は0pt plus 1filに設定する。
  • \nulldelimiterspaceは「区切り記号(大型括弧)があるべき箇所に実際に何もない場合に代わりに置かれる空き」の大きさ。今回の出力では分数の前後にこの空きが入る9
  • \delimiterfactor\delimitershortfallは区切り記号の大きさを決定するのに使われるパラメタ。根号の大きさをplainに合わせるために設定した。

6行目

\thinmuskip=3mu \medmuskip=4mu \thickmuskip=5mu

これらは記号の周りに自動的に入る空きの大きさを決めるパラメタである。今回の数式の中では、“±”と“−”の周りの空きが\medmuskip、“=”の周りの空きが\thickmuskipである。(\thinmuskipは使われていないので設定は不要だった🙃)

これらのパラメタは“mu”(math unit)という「数式用フォントのサイズに基づく相対単位」で表す(現在の数式スタイルでのファミリ2のフォントの1emが18muに等しい)。

plainでは伸縮付きの値(例えば\medmuskip=4mu plus 2mu minus 4mu)が設定されているが、今回は伸縮は不要なので外した。

7~8行目

\font\tt=cmr10 \font\st=cmr7 \tt\fam0
\font\tm=cmmi10 \font\ts=cmsy10 \font\tx=cmex10

必要なフォント(fontdefトーク)の定義をしている。今回のコード中に現れるプリミティブでない制御綴はここで定義されるもの(\tt\st\tm\ts\tx)しかない10

7行目末尾の\ttはテキストのフォントをcmr10に設定している(初期状態のフォントは\nullfont11である)。実際にはテキスト(数式以外)の文字は一切出力していないが念のため設定した。\fam0は「“現在の数式ファミリ”(つまり“数式英字フォント”として使われる数式ファミリ)を0番に設定する」という意味だが、これも不要であった🙃12

9~12行目

\textfont0=\tt \scriptfont0=\st \scriptscriptfont0=\st
\textfont1=\tm \scriptfont1=\tm \scriptscriptfont1=\tm
\textfont2=\ts \scriptfont2=\ts \scriptscriptfont2=\ts
\textfont3=\tx \scriptfont3=\tx \scriptscriptfont3=\tx

数式ファミリ(math family)にフォント(fontdefトークン)を割り当てている。\textfontで通常サイズ、\scriptfontで添字用の小さいサイズ、\scriptscriptfontで二重添字サイズのフォントを指定する。

ここではplainと同様にファミリ0にcmr、ファミリ1にcmmi、ファミリ2にcmsy、ファミリ3にcmexを使っているが、添字用(小さいサイズ)のものは実際に必要なもの(式の中に上添字の“2”があるので\scriptfont0は必要)以外は別のサイズのもので代替している。

例えば、式の先頭の“x”(数式コード "7178)は通常サイズのファミリ1、すなわち\textfont1で出力されるが、その\textfont1\tm、すなわちcmmi10である。

ちなみに、初期状態では全てのファミリのフォントが未定義(\nullfont)になっている。使用しないファミリは未定義でかまわないのだが、例外的にファミリ2と3については全てのサイズのフォントが定義済である必要がある13

13行目

\output{\shipout\vbox to\vsize{\vfill\unvbox255}}

出力ルーチン\outputトークン列レジスタ)を設定している。

\outputの初期値は空でこの場合は「既定の出力ルーチン」である

\shipout\box255

が使われることになっている。これは「TeXのページ分割の結果作られたページ(255番のボックスレジスタの中身)をそのままDVIに出力する」という処理を意味している。

今回のコードでは版面の垂直方向の中央に数式を出したいので、「\box255の中身の前に\vfillを追加した上で\shipoutする」という出力ルーチンを実装した14

ここまでのコードで全ての設定が完了したことになる。

14行目

$$x={-b\mathchar"2206\radical"270370{b^2-4ac}\over2a}$$

「解の公式」の数式を出力するコードである。比較のために、plain TeXで同じ数式を普通に書いた場合のコードを以下に示す。

$$x={-b\pm\sqrt{b^2-4ac}\over2a}$$

このplainのコード中に現れる制御綴のうち、\overはプリミティブであるが\pm\sqrtは“plainで定義されたもの”である。従って、iniTeXでは同等の機能をプリミティブだけで書く必要がある。

  • \pm\mathchardef\pm="2206で定義されるmathchardefトークである15。従って、\mathchar"2206で同じ動作になる。
  • \sqrt\def\sqrt{\radical"270370 }で定義されるマクロである。従って、単純にマクロの本体で置き換えればよい。

このように書き換えると最初に挙げたiniTeXのコードができあがる。既に“plainと同じ”になる設定が行われているので、このコードで“plainと同じ”の「解の公式」が出力される。

参考として、ここで用いたコードの意味を説明しておく。

  • \mathchar"2206は「数式コードが "2206 の文字を実行する」のと同等である。すなわち「二項演算子(2)として、数式ファミリ2のフォント(ここではcmsy10)の文字コード "06 のグリフを出力する」という動作になる。
  • \radical根号を出力するためのプリミティブであり、根号は“伸長可能なグリフ”と上線の組み合わせで構成される。引数の "270370 のうち、前3桁の "270 は通常のグリフの位置(数式ファミリ2のフォントの文字コード "70)、後3桁の "370 は大型のグリフ16の位置(数式ファミリ3(ここではcmex10)のフォントの文字コード "70)を表している。

15行目

\end

TeXの実行を終了させるプリミティブは\endである17。これにより以下の処理が行われる。

  • まだメモリに残っている内容をDVIに出力する。
    • ページビルダを実行して「解の公式」の数式を含むページの内容を\box255に格納する。
    • 出力ルーチン(先ほどの\outputトークン列)を実行する。その中の\shipoutによりボックスの内容(「解の公式」の数式)がDVIに出力される。
  • TeXの実行を終了する。

めでたしめでたし😊

まとめ

というわけで、「LaTeXなんて“本当のTeX”ではない」と主張する人は、“本当のTeX”であるiniTeXについてもっと学習しましょう!💁


  1. もちろん「TeX処理系およびその言語」というのが本来の意味であるが、でももしそれに従うのであれば、「LaTeXのコード」も「plain TeXのコード」も間違いなく「TeXのコード」といえるはずである。
  2. 「plainフォーマットのTeX」のことを「plain TeX」と呼ぶのは後代の用語である、という話を聞いたことがある。
  3. plainやLaTeX等の“フォーマット”の実装コードを何も読み込まずに、本当にTeXの「初期状態」で起動するモードのこと。初期のTeX配布物では本体のTeXとは別のソフトウェアになっていてそれを“iniTeX”と呼んでいたのだが、後にiniTeXの機能をTeXに組み込んで「TeXのINIモード」として扱うようになった。
  4. 例えば、「解の公式」の数式の中にはb^2があるので^は設定する必要がある。
  5. 実は、“+”の記号は「解の公式」の中で全く使っていないので、+の数式コードの設定は不要であった。
  6. ちなみに、cmr10の文字コード"2D(ASCIIのhyphen-minusの位置)にあるグリフはマイナスではなくハイフンである。
  7. TeXのパラメタの初期値(初期状態の値)は大抵はゼロ(0、0pt、0mu)である。
  8. パラメタ\hoffset\voffsetが初期値の0ptのままで、これに“例の1インチ”が加わる。
  9. TeXの“汎化分数”のプリミティブ(\abovewithdelims)はそれ自身に大型括弧を付ける機能があったことを思い出そう。
  10. なお、TeXの初期状態ではプリミティブ以外の制御綴は全て未定義の状態である。
  11. プリミティブとして用意されている、“全くグリフが定義されていない”ようなfontdefトークン。
  12. \famパラメタは数式の開始時に常に−1にリセットされるため数式の外で設定しても意味がない。そもそも\famの初期値は0である。
  13. 未定義のまま数式を入力するとMath formula deleted: Insufficient symbol fonts.というエラーが出る。これらのファミリの「フォントのパラメタ」が数式全体のパラメタとして参照されるからである。
  14. \vfillを前にだけ入れている理由は、\box255の末尾に既に\vfillが入っているからである。この\vfill\endの処理の中で挿入されるようである。
  15. \mathchardef\X=‹整数n›で定義されるmathchardefトーク\Xを実行すると\mathchar‹整数n›と同等の動作になる。特定のコードの代わりになるという点ではmathchardefトークンはマクロと似ているが、マクロとは異なり展開不能である。なお、mathchardefトークンと\mathcharの関係は、chardefトークンと\charの関係と同じである。
  16. もちろん、TeXの数式中の根号は2段階ではなくもっと多くの段階をもって伸長できる。この「伸長によるグリフの置換・再配置」はTFMの内部の情報を使って処理されている。括弧類についても同様である。
  17. なお、plainの\bye\par\vfill\supereject\endに展開されるマクロである。