この記事は SATySFi Advent Calendar 2020 の 5 日目の記事です。
昨日は na4zagin3 さんによる「2020 年 Satyrographos 周辺の動きと次メジャーアップデートに向けた展望」でした。
明日は puripuri2100さんによる「SATySFiでトンボと裁ち落としを設定するパッケージを作った話」です。
(La)TeXで湯婆婆を実装してみる という記事があったので。こりゃ SATySFi でもやらなアカンということで作りました。ネタ記事です。
SATySFi とは
熱心な湯婆婆ファンの中には SATySFi のことをご存知でない方もいると思いますので、SATySFi という言語について簡単に説明します。
SATySFi とは、2018年に開発された新しい組版処理システムです。組版処理システムといえばTeX/LaTeXが有名ですが、SATySFi は LaTeX 風のマークアップ用構文と、 OCaml 風のプログラミング用構文を兼ね備えています。SATySFi 自体は静的型付けの関数型言語であるため
- 強力な静的解析
- 理解しやすいエラーメッセージ
といった強みがあり、TeX/LaTeX の代替としての役割が期待されています。本記事執筆時点でのバージョンはまだ 0.0.5
ですが、現時点でもある程度実用的な原稿を組むことができます。
より詳しく知りたい方は、以下のリンクの記事がおすすめです。ぜひ、 SATySFi advent calendar 2020 で公開されている他の記事も読んでみてください。
- 公式 README
- SATySFi wiki
- https://qiita.com/puripuri2100/items/a63e0c48f245dfcb0446
- https://qiita.com/na4zagin3/items/a6e025c17ef991a4c923
仕様
元ネタの Javaで湯婆婆を実装してみる に可能な限り準拠することにします。
コード
以下のコードを yubaba.saty
という名前で保存します。
@require: pervasives
@require: base/random
@require: base/list-ext
let-inline ctx \math m = script-guard Latin (embed-math ctx m)
let-block ctx +p it = line-break true true ctx (read-inline ctx it ++ inline-fil)
let-inline ctx \textbf it =
let ctx = ctx
|> set-font Kana (`fonts-noto-serif-cjk-jp:NotoSerifCJKjp-Bold`, 1.0, 0.0)
|> set-font HanIdeographic (`fonts-noto-serif-cjk-jp:NotoSerifCJKjp-Bold`, 1.0, 0.0)
in
read-inline ctx it
let dialogue name name-abbrev =
'<
+p{
湯婆婆「契約書だよ。そこに名前を書きな。」
}
+p{
氏名:#name;
}
+p{
湯婆婆「
フン。#name;というのかい。贅沢な名だねえ。
今からお前の名前は#name-abbrev;だ。いいかい、#name-abbrev;だよ。
分かったら返事をするんだ、#name-abbrev;!
」
}
>
let yubabify ctx name seed =
let _ = () |> List.repeat seed |> List.map Random.random in
let name-str = extract-string (read-inline ctx name) in
let n = string-length name-str in
let idx = (Random.random ()) mod n in
let char = string-sub name-str idx 1 in
embed-string char
let document record =
let ctx =
get-initial-context 440pt (command \math)
|> set-dominant-narrow-script Latin
|> set-dominant-wide-script Kana
|> set-font Kana (`fonts-noto-serif-cjk-jp:NotoSerifCJKjp-Regular`, 1.0, 0.0)
|> set-font HanIdeographic (`fonts-noto-serif-cjk-jp:NotoSerifCJKjp-Regular`, 1.0, 0.0)
|> set-language Kana Japanese
|> set-language HanIdeographic Japanese
|> set-language Latin English
|> set-hyphen-penalty 100
|> set-font-size 10pt
|> set-paragraph-margin 10pt 10pt
in
let bt = dialogue record#name (yubabify ctx record#name record#seed) in
let bb = read-block ctx bt in
page-break A4Paper
(fun _ -> (| text-origin = (80pt, 100pt); text-height = 630pt; |))
(fun _ -> (|
header-origin = (0pt, 0pt);
header-content = block-nil;
footer-origin = (0pt, 0pt);
footer-content = block-nil;
|))
bb
in
document (|
name = {荻野千尋};
seed = 1;
|)
実行には satysfi-base パッケージ 及び Noto Serif CJK JP が必要ですので、必要に応じてインストールしてください。Satyrographos を使えば以下のコマンドを走らせることで簡単にインストールできます。
opam install satysfi-base satysfi-noto-serif-cjk-jp
satyrographos install
SATySFi の環境が整えば、シェルで以下のコマンドを走らせることができます。
satysfi yubaba.saty
うまく行けば同ディレクトリに以下のような PDF が生成されるはずです。
ちゃんと原作のやり取りが再現されていますね! seed
の値を変えれば湯婆婆の挙動も変わるので、好きな0以上の整数値を設定してみてください。
手軽に上のコードを試したい人へ
湯婆婆コード試してみたいけど手元のPCでSATySFiの環境構築するの面倒くさい…そんな人へ。Gitpod を使えば簡単に試すことができます。
詳しくは monaqa/satysfi-yubaba 参照。
コードの解説
上からコードを説明します。
@require: pervasives
@require: base/random
@require: base/list-ext
ここは SATySFi の「ヘッダ」と呼ばれる箇所で、必要なパッケージをインポートしています。
let-block ctx +p it = line-break true true ctx (read-inline ctx it ++ inline-fil)
let-inline ctx \math m = script-guard Latin (embed-math ctx m)
let-inline ctx \textbf it =
let ctx = ctx
|> set-font Kana (`fonts-noto-serif-cjk-jp:NotoSerifCJKjp-Bold`, 1.0, 0.0)
|> set-font HanIdeographic (`fonts-noto-serif-cjk-jp:NotoSerifCJKjp-Bold`, 1.0, 0.0)
in
read-inline ctx it
ここでは、後に登場するコマンドを定義しています。+p
は段落を組むコマンド、\math
は数式を組むコマンド、\textbf
はテキストを太字にするコマンドです。
let dialogue name name-abbrev =
'<
+p{
湯婆婆「契約書だよ。そこに名前を書きな。」
}
+p{
氏名:#name;
}
+p{
湯婆婆「
フン。#name;というのかい。贅沢な名だねえ。
今からお前の名前は#name-abbrev;だ。いいかい、#name-abbrev;だよ。
分かったら返事をするんだ、#name-abbrev;!
」
}
>
今回のコードの1つ目の肝です。ここは原作のセリフを再現する関数です。name
と name-abbrev
という2つのインラインテキストを引数にとり、いくつかの段落からなるブロックを作成します。SATySFi ではグラフィックを入れたり文字を枠で囲んだりすることもできますから、契約書が簡素すぎるなあと感じた方はぜひ上の関数をいじってよりファンシーにしてみてください。
let yubabify ctx name seed =
let _ = () |> List.repeat seed |> List.map Random.random in
let name-str = extract-string (read-inline ctx name) in
let n = string-length name-str in
let idx = (Random.random ()) mod n in
let char = string-sub name-str idx 1 in
embed-string char
今回のコードの2つ目の肝です。ここは湯婆婆の魔法で名前を奪う関数です。name
引数に与えたインラインテキストから、適当な1文字を取り出したインラインテキストを返します。base/random
にある Random
モジュールの random
関数を用いて、乱数を生成しています。Random.random
は副作用を持つ関数であり、呼び出すたびに結果が変わります(ランダムな値を生成する関数のため、当然の仕様といえます)。今回は値にバリエーションを持たせるため、 Random.random
関数を予め seed
で与えた回数だけ呼び出してから適当な乱数を取得することにしました。
let document record =
let ctx =
get-initial-context 440pt (command \math)
|> set-dominant-narrow-script Latin
|> set-dominant-wide-script Kana
|> set-font Kana (`fonts-noto-serif-cjk-jp:NotoSerifCJKjp-Regular`, 1.0, 0.0)
|> set-font HanIdeographic (`fonts-noto-serif-cjk-jp:NotoSerifCJKjp-Regular`, 1.0, 0.0)
|> set-language Kana Japanese
|> set-language HanIdeographic Japanese
|> set-language Latin English
|> set-hyphen-penalty 100
|> set-font-size 10pt
|> set-paragraph-margin 10pt 10pt
in
let bt = dialogue record#name (yubabify ctx record#name record#seed) in
let bb = read-block ctx bt in
page-break A4Paper
(fun _ -> (| text-origin = (80pt, 100pt); text-height = 630pt; |))
(fun _ -> (|
header-origin = (0pt, 0pt);
header-content = block-nil;
footer-origin = (0pt, 0pt);
footer-content = block-nil;
|))
bb
ここは全体のドキュメントを作成するためのコマンドですが、ややこしいので適当に読み飛ばしても構いません。途中の dialogue record#name (yubabify ctx record#name record#seed)
という部分で、上で説明した2つの関数を呼び出しています。
実際に SATySFi を用いる場合、このあたりの設定はクラスファイル(stdja
パッケージなど)が勝手に行ってくれるため、通常の SATySFi の文書ではこういったコマンドを書く必要はほぼありません。今回はクラスファイルを用いず原作を再現したため、このような設定を書いています。
document (|
name = {荻野千尋};
seed = 1;
|)
document
関数を呼び出し、実際に文書を生成します。name
で名前を、 seed
で乱数のシードを指定します。
名前を変えてみる
「千と千尋の神隠し」では名前が千尋でしたが、それ以外の名前にも対応していなければ完全な湯婆婆とはいえません。ちゃんと対応しているかどうか確かめましょう。
ふん。获野千尋というのかい。
実は、原作において主人公の荻野千尋は自分の名前を「获野千尋」と書いています。
わかりにくいですが、一文字目が「荻」(右下が"火")ではなく「获」(右下が"犬")になっています。「获」になっている理由はいろいろな考察が行われているようですが、いずれにせよ今までの例が原作を忠実に再現したものではなかったことは確実です。ちゃんと原作を再現しなければなりません。
幸い、SATySFi では書きたい文字に対応するグリフのあるフォントさえ選んでおけば(今回は Noto Serif CJK JP を用いました)、簡単に対応することができます。
document (|
% name = {荻野千尋}; ※ TeX 同様、% からは行末までコメントアウトされる
name = {获野千尋};
seed = 1;
|)
なお、他の湯婆婆実装だと「𠮷田」という例が多いですが、𠮷田さんにもちゃんと対応しています。これで、全国の𠮷田さんも安心して神隠しできるようになりました。
ふん。SATySFiというのかい。
LaTeX という名前が、より正式には $\mathrm{\LaTeX}$ と表記されるように、SATySFi にも正式な(?)組み方があります。SATySFi では、 pervasives
パッケージの \SATySFi;
コマンドを用いて組むことができます。
document (|
name = {\SATySFi;};
seed = 1;
|)
もし映画「AとSATySFiの神隠し」が今後公開される運びとなっても、これなら問題なく対処できますね。
ふん。というのかい。
SATySFi は、可能な限り静的解析の段階でエラーを検出しよう、という思想で設計されています。たとえば1000ページもあるような大規模な文書をタイプセットするとき、995ページ目まで組んでしまってからエラーを検出して処理を中断してしまうと、それまでの処理に費やした時間が全て無駄になってしまうためです。
そんな理由によって SATySFi はあまり実行時エラーを出さないのですが、上のコードで name
に空のインラインテキストを与えると(少なくともv0.0.5
時点では)実行時エラーとなります。Javaで湯婆婆を実装してみる の「名前が空だと実行時エラーになる」という要件も再現されていますね。素晴らしい!(?)
document (|
name = {};
seed = 1;
|)
! [Error during Evaluation] division by zero
これはエラーメッセージを見ると明らかなように、ゼロ除算が原因です。
let n = string-length name-str in
let idx = (Random.random ()) mod n in
という箇所で (Random.random ())
によって得られた整数の n
で割った余りを求めていますが、name-str
が空文字列だと n
が0になってしまい、ゼロ除算エラーが起きるというわけです。SATySFi では型検査を行うものの (int) mod (int)
という演算自体は定義されており、1 mod 0
といった演算は型検査でも引っかかりません。従ってゼロ除算は(少なくとも現時点では)実行時エラーとなるように設計されています。
ふん。\textbf{获野千尋}
というのかい。
原作での千尋は普通のペンを使って名前を書いたものの、千尋が極太サインペンしか持っていないケースも当然考えられます。そのような場合は、入力を
document (|
name = {\textbf{获野千尋}};
seed = 1;
|)
とするのが至極真っ当ですね。試してみましょう。
契約書と、湯婆婆による本名の読み上げのときは正しく名前が太字になりましたが、その後は通常のウェイトに戻ってしまいました。これは、湯婆婆の魔法のときに「一度インラインテキストを文字列型に変換する」という作業を挟むことによって、フォントに関する情報が失われてしまうためです。
湯婆婆の魔法は、フォントのウェイト等といった文脈をも消し去ってしまうほどに強力なのかもしれません。
まとめ
SATySFi を使えば、誰でも簡単に湯婆婆することができます。みなさんも Let's SATySFi!