本記事は Hello World あたたたた Advent Calendar 2025 の記事です.
今日はMETAFONTで「Hello World あたたたた」を実装して解説していきます.
そもそも「Hello World あたたたた」が何かは 1日目の記事 をご参照ください.
コーディング例
オンライン実行環境はないため, ローカルに実行環境をお持ちの方は手元で試してみてください. (TeX Liveがインストールされていれば実行できるはずです.)
numeric seed_tmp;
if unknown seed:
seed_tmp := time * 2.843;
seed_tmp := seed_tmp + (day * 0.088);
seed_tmp := seed_tmp + (month * 0.006);
seed_tmp := seed_tmp + ((year mod 100) * 0.00005);
else:
seed_tmp := seed;
fi;
randomseed := seed_tmp;
string hako, current_char;
hako := "";
boolean running;
running := true;
forever:
numeric x;
x := floor(uniformdeviate 2);
if x = 0: current_char := "あ"; else: current_char := "た"; fi;
message current_char;
hako := hako & current_char;
if length(hako) >= 15:
if substring (length(hako)-15, length(hako)) of hako = "あたたたた":
message "";
message "お前はもう死んでいる";
running := false;
fi;
fi;
exitif not running;
endfor;
end.
実行方法
内部処理により生成されたシード値を使用する場合は以下のように実行します. ただし, シード値は分単位でしか変化しません.
$ mf atatatata.mf
また, 外部からシード値を与えて実行する場合は以下のようにワンライナーで実行します. ただし, シード値は $S\in[0,4096)$ を満たす実数 $S$ とします.
$ mf "\numeric seed; seed:=$(( $(date "+%Y%m%d%H%M%S") % 4096 )); input atatatata.mf"
コードと文法の解説
変数の宣言と代入
string hako, current_char;
hako := "";
boolean running;
running := true;
METAFONTでは, 変数の宣言時に型を指定する必要があります. ここでは、hako と current_char を文字列型 (string), running を論理値型 (boolean) として宣言しています. また, 代入には := を使用します.
type-declaration primitives
primitive ("numeric", type name , numeric type );
primitive ("string", type name , string type );
primitive ("boolean", type name , boolean type );
primitive ("path", type name , path type );
primitive ("pen", type name , pen type );
primitive ("picture", type name , picture type );
primitive ("transform", type name , transform type );
primitive ("pair", type name , pair type );
シード値生成
numeric seed_tmp;
if unknown seed:
seed_tmp := time * 2.843;
seed_tmp := seed_tmp + (day * 0.088);
seed_tmp := seed_tmp + (month * 0.006);
seed_tmp := seed_tmp + ((year mod 100) * 0.00005);
else:
seed_tmp := seed;
fi;
randomseed := seed_tmp;
METAFONTには year, month, day, time というプリミティブ (内部的な量) が存在しますが, これらは最小で分単位の精度しか持っていません. そのため, 分単位でしかシード値を変化させることができません.
time primitives
primitive ("year", internal quantity , year );
primitive ("month", internal quantity , month );
primitive ("day", internal quantity , day );
primitive ("time", internal quantity , time );
そこで, より高いランダム性を確保するために, 実行方法で示したようにBashなどのシェル側で秒単位の時刻を取得し, ワンライナー実行でMETAFONTの変数 seed に流し込む手法をとっています.
$ mf "\numeric seed; seed:=$(( $(date "+%Y%m%d%H%M%S") % 4096 )); input atatatata.mf"
このようなワンライナーでの実行に関しては, 『The METAFONTbook』でも言及されています.
Incidentally, many systems allow you to invoke METAFONT by typing a oneline command like ‘mf io’ in the case of Experiment 2; you don’t have to wait for the ‘**’ before giving a file name. Similarly, the one-liners ‘mf \relax’ and ‘mf \mode=smoke; input io’ can be used on many systems at the beginning of Experiments 1 and 3. You might want to try this, to see if it works on your computer; or you might ask somebody if there’s a similar shortcut.
-- Donald E. Knuth, 『The METAFONTbook』 Chapter 5 より引用
シード値の決定規則
本実装では, unknown を使うことで, 外部から seed が与えられた場合とそうでない場合の両方に対応しています.
外部入力 $S_{\text{external}}$ (秒単位の時刻など) が与えられた場合は,
S=S_{\text{external}}
とし, 外部入力が与えられない場合は,
S=S_{\text{internal}}
としています. ここで, $S_{\text{internal}}$ は
S_{\text{internal}} = \alpha t + \beta d + \gamma m + \delta(y \bmod 100)
として定義しています. なお, 各変数は
- $t\in\lbrace 0,\ldots,1439\rbrace$:0:00からの経過分数
- $d\in\lbrace 1,\ldots,31\rbrace$:日
- $m\in\lbrace 1,\ldots,12\rbrace$:月
- $y\bmod 100\in\lbrace 0,\ldots,99\rbrace$:年の下2桁
です.
係数設計
METAFONTのnumeric型は固定小数点数であり, すべての数値リテラルは $1/65536$ の倍数に丸められて解釈されます. また, 表現可能な範囲は概ね
0\leq S<N_{\max},\quad N_{\max}\approx 4096
です.
この制約の下で, 各成分の影響が上位成分と干渉しないように各係数を次の条件に基づき選定しています.
-
時間項
- $\alpha \approx 2.843$
- 時刻 $t$ は1日の分数として
を取ります
t \in \{0,1,\dots,1439\} - $t$ の定義域を数値範囲全体にほぼ一様に展開するため,
としています
\alpha \approx \frac{N_{\max}}{1440} - これにより,$t$ を1分進めるごとに $S_{\text{internal}}$ はおよそ $\alpha$ だけ増加します
-
日付項
- $\beta \approx 0.088$
- 日付 $d$ は
を取ります
d \in \{1,\dots,31\} - 異なる時刻 $t$ に対応する値が衝突しないためには, $t$ を1増加させたときに生じる数値的増分 $\alpha$ が作る区間内に, 日付による最大変動
が完全に収まる必要があります
31\beta - この条件
を満たす最大の値として $\beta$ を選定しています
31\beta < \alpha
-
月項・年項
- $\gamma\approx 0.006$, $\delta\approx 0.00005$
- 月 $m\in\lbrace 1,\dots,12\rbrace$, 年 $y\bmod 100\in\lbrace0,\dots,99\rbrace$ に対しても同様に, 上位成分の1ステップ増分が作る値域の中に下位成分の変動が完全に収まるよう係数を定めています
- 具体的には,
を満たす最大の値として $\gamma,\delta$ を選定しています
12\gamma < \beta,\qquad 100\delta < \gamma
以上より, 各係数は次の包含関係を満たします:
\underbrace{\alpha}_{\text{時刻 1 分の増分}}>\underbrace{31\beta}_{\text{日付の最大変動}}>\underbrace{12\gamma}_{\text{月の最大変動}}>\underbrace{100\delta}_{\text{年の最大変動}}
この関係は, 時刻・日付・月・年が数値的に辞書式順序で埋め込まれていることを意味し, 異なる時刻・日付の組に対して $S_{\text{internal}}$ が衝突しないことを保証します.
メインループ
forever:
numeric x;
x := floor(uniformdeviate 2);
if x = 0: current_char := "あ"; else: current_char := "た"; fi;
message current_char;
hako := hako & current_char;
if length(hako) >= 15:
if substring (length(hako)-15, length(hako)) of hako = "あたたたた":
message "";
message "お前はもう死んでいる";
running := false;
fi;
fi;
exitif not running;
endfor;
ループ構造には forever と endfor を使い, 終了条件を exitif で記述しています.
なお, METAFONTのイテレーションには, 他にも for と forsuffixes が存在しますが, ここでは無限ループにするために forever を使用しています.
iteration primitives
primitive ("for", iteration, expr base );
primitive ("forsuffixes", iteration, suffix base );
primitive ("forever", iteration, start forever );
primitive ("endfor", iteration, end for );
乱数生成
numeric x;
x := floor(uniformdeviate 2);
if x = 0: current_char := "あ"; else: current_char := "た"; fi;
乱数生成には, uniformdeviate を使用しています. uniformdeviate x は, $x>0$ のとき $0 \le u < x$, $x<0$ のとき $0 \ge u > x$ に一様分布する乱数 $u$ を返し, $x=0$ のときは常に $u=0$ を返します. これを floor で切り捨てることで, $0$ または $1$ を等確率で得ています.
\text{current_char} =
\begin{cases}
\text{"あ"} & \text{if}\,\, 0 \le U < 1,\\
\text{"た"} & \text{if}\,\, 1 \le U < 2,
\end{cases}
\qquad
U \sim \mathrm{Uniform}[0,2)
文字列結合
hako := hako & current_char;
文字列の結合には & を使用しています.
終了判定
if length(hako) >= 15:
if substring (length(hako)-15, length(hako)) of hako = "あたたたた":
message "";
message "お前はもう死んでいる";
running := false;
fi;
fi;
exitif not running;
METAFONTの length や substring は, マルチバイト文字を考慮せず, 単純にバイト単位で処理を行います. しかし, UTF-8エンコーディングにおいて, ひらがなの「あ」や「た」は通常3バイトで表現されます. そのため, 蓄積された文字列 hako の末尾15バイトを substring により抽出し, 「あたたたた」と比較しています.
なお, 文字列の出力には message を使用しています.
METAFONTの歴史
- 1977年:proto-METAFONT
- Computer Modernを設計するために, SAILのサブルーチンとマクロ群としてDonald E. Knuthが試作
- フォント設計専用の言語は存在せず, 字形の変更にはSAILプログラムの再コンパイルが必要だった
- 1979年:METAFONT79
- Computer Modernの記述をより自然かつ簡潔に行うため, 解釈型言語としてのMETAFONTを設計
- Knuthにより, SAILで完全なMETAFONT処理系が実装された
- Leo Guibas, Lyle Ramshaw, David Fuchsにより, さまざまな組版装置や表示端末向けに移植・改良が行われた
- 1982年:転換期
- Computer Modernの大規模な設計改良が行われた
- METAFONT79では, 文字設計者の要求を十分に表現できないことが明らかとなり, 新しい言語設計の必要性が認識された
- 1984年:METAFONT84
- 言語仕様およびシステムを全面的に再設計した新しいMETAFONTを開発
- 名称と理念の一部はMETAFONT79を継承するが, 内部仕様や詳細はすべて変更された
- TEX82と同一の設計規約を採用
- 可搬性と再現性を重視し, WEBによる記述からPascalへ変換する構成を採用
- 多様な計算機環境で同一の出力が得られることを目標とした
- 1984年以降:凍結と安定化
- KnuthはMETAFONT84を「凍結」し, 今後は安定性と信頼性を最優先とする方針を明言
- 中核仕様を変更せず, WEBレベルでの拡張のみを許容
- 完全互換な実装のみが「METAFONT」と名乗ることを許される
- 互換性検証のための公式テストスイートとしてTRAP testが用意された (1986年)
個人的な感想
METAFONTは本来, フォントを作成するプログラムですが, 柔軟なフォント設計のために強力な計算系を内包しています. そのため, 本プログラムの実装においては, BibTeX (BAFLL/BeaST) よりも実装しやすかったです.