こんにちは。hsjoihs (はすじょい)です。
ZEN 大学で「オートマトンと形式言語理論」と「論理回路概論」の 2 科目を担当する予定の、教員予定者です。
宣伝
以上、「論理回路概論」にまつわる宣伝でした。
以降、「オートマトンと形式言語理論」の方の話だけをします。
やりたいこと
当時の #times_hsjoihs を見ると、こんなことが書いてありました。
hsjoihs (はすじょい) ― 2024/01/22 09:49
「形式言語の文字列を組み立てるときには、ちゃんとその構造を見てくれるツールを使って組み立てるべきで、さもないとインジェクション攻撃とかの良くない諸々を喰らうぞ」という話を教材に書いておきながら、SVG を JavaScript のテンプレート文字列を使って組み立てているのが完全に hypocrite(和訳:「紺屋の白袴」)なので、JSX-but-not-React で書き直したいんだよな
このままだと文脈が不足しすぎていますね。詳しく説明します。
職務
私の現在の職務は、わりと敬遠されがちな科目である「オートマトンと形式言語理論」を、いかにして「自発的にコードを書くほどプログラミングに比較的手慣れているが、大学数学は未経験か不慣れである」という人々に対して分かりやすくお伝えするか、というのをひたすら考える仕事です。
詳しい話は、Nextbeat Tech Bar:第一回プログラミング教育について語る会で語ったので、ご興味あればスライドをご覧ください。
状態遷移図を programmatical に作る
さて、「オートマトンと形式言語理論」という授業を展開する上では、状態遷移図というものを必然的に紹介する必要があります。
弊部署では、教材内にイラストや図を載せたいときには基本的に部署内に依頼を投げる形となっているのですが、こと状態遷移図に関しては
- とにかく必要とする枚数が多い(2024/12/13 現在で 291 枚使用)
- デザインにある程度の統一感がないといけない
- 教材執筆者である私が、必要に応じて・速やかに・臨機応変に図の内容や構成を変更する必要がある
という都合上、programmatical な手段を取る必要がありました。
2023/08/01 に教材を書き始め、早々にこのことに気づいた私は、2023/08/03 に仮デザイン案を組み、以下のような相談をしました。
配色に関して相談がございます。
オートマトンというものを説明するために、状態遷移図というものを教材内にかなりの個数用意する必要があります。とりあえず仮置きで
- 矢印・矢印に付随するテキスト・丸の枠線は blue (#0000FF)
- 丸の背景色は green (#008000)
- 丸の中に書くテキストは white (#FFFFFF)
で作ってみたものが添付の nico.svg になります。
要件といたしましては、
- 色覚多様性(色覚異常)に配慮する
- 図全体に背景色を設定せずとも、ダークモードでもライトモードでも気にならずに図が知覚できる
- ダークモードでもライトモードでも、それぞれの丸が「一重丸」「二重丸」であることが一目で分かる
- ダークモードでもライトモードでも、矢印に付随するテキストが読みやすい
- 「丸の背景と丸のテキスト」のコントラストが確保されており、文字が読みやすい
- 見ていてたのしい(主観)
を満たすことができますと理想的でございます。
色の選定をお願いできますでしょうか。また、青と同じ意味の部分を強調して目立たせたいとき、現状は orange (#FFA500) を選定しています。これも、上記要件を満たしつつ現状の青と明確に区別できる色がほしいので、選定をお願いしたいです。
もし配色の選定だけでなく全体的なデザインまでしていただける場合は、SVG 形式で頂けますとありがたいです!
SVG
ここで、私は SVG (Scalable Vector Graphics) 形式でのやり取りを求めています。
SVG についての詳しい解説は MDN をお読みいただければと思いますが、まあ要するに XML を使って図が書けるという素晴らしい形式です。
SVG is, essentially, to graphics what HTML is to text.
具体例を見せたほうが分かりやすいと思うので、一例だけ。
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<path d="M 10 10 H 90 V 90 L 10 10" fill="gray"/>
<circle cx="10" cy="10" r="10" fill="red"/>
<circle cx="90" cy="90" r="10" fill="green"/>
<circle cx="90" cy="10" r="10" fill="blue"/>
<text x="60" y="40" font-size="25" text-anchor="middle" fill="white">SVG</text>
</svg>
というテキストファイルを example.svg という名前で保存して、Web ブラウザなどで開いてみると、こんな感じで表示されるかと思います。
こんなふうに、描きたい図をテキスト形式で表現でき、たとえば GitHub とかで差分管理をする際にも非常に重宝します。
SVG を選定した理由
SVG を選定した理由は以下の 3 つです。
理由その①:Adobe Illustrator は SVG を読み書きできる
まず、非常に重要な点として、Adobe Illustrator は SVG を入出力のためのフォーマットとして使えるというのがあります。
これはつまり、Adobe Illustrator を日常的に使って作業している方にデザイン案を依頼する際に、「この SVG を編集していただいて、結果を SVG 形式でください」というふうに、コミュニケーション手段として使えるということです。
(ただし、「SVG 内の CSS のクラス名などにアンダースコア (_
) を使うと Adobe Illustrator が読んでくれない」などの罠に苦しめられたりする羽目にはなりました。)
hsjoihs (はすじょい) ― 2023/10/02 18:34
なぬ~~ Adobe Illustrator は識別子名に _ が入っていると正しく読んでくれないからハイフンにしろだと~~
https://illustrator.uservoice.com/forums/333657-illustrator-desktop-feature-requests/sugg[…]ns/37262221-imported-svg-path-elements-always-black-fill
ほんとだ!!! なおった!!!!!
理由その②:ブラウザは SVG を解釈・表示できる
caniuse.com を見る限り、 あらゆるモダンブラウザは SVG を解釈・表示できます。
私の教材の想定閲覧環境は Web (Web ブラウザまたは iOS/Android アプリケーションの WebView)なので、都合がよいですね。
理由その③:私がそもそも SVG に比較的慣れ親しんでいる
学生時代からやっている同人ボドゲなどをやっているサークルが、ルールブックやら箱やらを全て S_Y15 氏が SVG で組み上げる1ことによってまわっており、「盤面の SVG をいちいち Inkscape で組み上げるのが大変だから自動化してほしい」などの issue を受けて
私がそれを実装していったり
したことをきっかけに、「SVG ってテキストエディタで結構手書きできるな…」と親しみを覚えたというのがあります。
最近は私も Inkscape を使って GUI で WYSIWYG に SVG を作ることのほうが多くはありますが、たまに全部手書きで SVG を書きたくなって書くこともあります。特に「なにをどこにどう配置したいか」が既に脳内で固まっていて数式でどう書けるかが全て事前に分かっている場合は、手書きのほうが便利ですね。
以上の 3 つの理由により、私は SVG を使うことにしたというわけです。
教材を書く際には、私はこのような値を TypeScript で書けばよく、
({
vertices: [
{ id: "q1", center: { x: 0, y: 0 } },
{ id: "q2", center: { x: 1, y: 0 } },
{ id: "q3", center: { x: 1.5, y: Math.sqrt(3) / 2 }, is_final: true },
{ id: "q4", center: { x: 0.5, y: Math.sqrt(3) / 2 } }
], initial_vertex: "q1", straight_edges: [
{ label: "ニ", from: "q1", to: "q2", config: { is_emphasized: true } },
{ label: "コ", from: "q1", to: "q4" },
{ label: "ニ", from: "q2", to: "q4" },
{ label: "コ", from: "q2", to: "q3", config: { is_emphasized: true } },
{ label: "ニ, コ", from: "q3", to: "q4" },
], self_loops: [
{ label: "ニ, コ", from_eq_to: "q4", config: { self_loop_rotation: 90 } },
]
})
それを受け取って変換して SVG として出力する、といった便利な使い方をしているのです。
さて、SVG の宣伝はこれぐらいにして、JSX の話をしましょう。
JSX
JSX は、要するに「JavaScript の中に XML みたいなものが書ける」というものです。
具体例を Writing Markup with JSX – React から引用すると、たとえば
export default function Avatar() {
const avatar = 'https://i.imgur.com/7vQD0fPs.jpg';
const description = 'Gregorio Y. Zara';
return (
<img
className="avatar"
src={avatar}
alt={description}
/>
);
}
というコードが書けます。
さて、SVG というのは XML なのでした。相性がよさそうですね。
ところで、私が求めているのは「SVG ファイルを出力してリポジトリにコミットする」ことなので、別に React は要りません。
要するに、先ほど紹介した「TypeScript 上の値を、SVG ファイルに変換する」という関数において、XML という代物の構造を踏まえずに JavaScript の素のテンプレートリテラルを使って
...(() => {
if (!g.stack) return [];
return [`<g id="stack" transform="translate(230 7)">` + g.stack.content.map((alphabet, i) => {
const color = i == 0 ? "rgb(185, 122, 87)" : "rgb(34, 177, 76)";
return `
<g id="stack${i}" transform="translate(0 ${-17 * i})">
<rect x="-24" y="-7" width="48" height="14" fill="none" stroke="${color}" />
<text fill="${color}" class="stack-symbol" x="0" y="0" dominant-baseline="central" text-anchor="middle">${alphabet}</text>
</g>`}
).join("") + `
</g>`]
})(),
というふうなコードを書いてしまっていることに対して、
「まあこの入力データを書くのも全て私だから、別にセキュリティ上の脅威ではないんだけど、それはそれとしてインジェクション攻撃が刺さるような問題あるコードになっているな~~」
という問題意識があったのです。
この問題意識は、なんなら「自動化しよう」と思ってコードを書き始めた初日から抱いていたものです。
hsjoihs (はすじょい) ― 2023/08/03 12:04
SVG こそ JSX で生成すべき代物な気がしてきたな
とはいえ、「まあ後回しでいいか。主務は教材執筆」などとやっており、その話を再び思い起こしたのは 2 ヶ月後の 2023/10/11 でした。
JSX-but-not-React
「React 抜きの JSX」という概念に不慣れな方も多いかと思われます。
「JSX って結局なに?(React の話は求めていません)」に対する簡潔な説明は、WTF is JSX - JASON Format がよくまとまっているので、そこから例を持ってきて紹介します。
たとえば、トランスパイラ Babel は、こういうコードを
/** @jsx h */
let foo = <div id="foo">Hello!</div>;
こういうコードにトランスパイルしてくれます。
/** @jsx h */
let foo = h("div", {
id: "foo"
}, "Hello!");
ということで、2024/01/22 に Slack の #times_hsjoihs で「JSX-but-not-React を導入した方がまだ読みやすくできるのではという気持ちになっている」と書いたところ、ありがたいことに berlysia さんから
最近だと hono/jsx が toString() するとそのまま文字列になってくれるのでよいかも
といただき、私は「まさに求めていたものである可能性が高いので、新機能組み終わったらさっそく導入を考えてみます」と返しました。
hono/jsx にはインデント付きで toString() する機能が無かったので、出力された XML を https://www.npmjs.com/package/xml-formatter に { indentation: " ", lineSeparator: "\n", collapseContent: true, forceSelfClosingEmptyTag: true }
で処理してもらう形にしました。
hono/jsx を使ってみた結果
before(一部抜粋)
...(() => {
if (!g.stack) return [];
return [`<g id="stack" transform="translate(230 7)">` + g.stack.content.map((alphabet, i) => {
const color = i == 0 ? "rgb(185, 122, 87)" : "rgb(34, 177, 76)";
return `
<g id="stack${i}" transform="translate(0 ${-17 * i})">
<rect x="-24" y="-7" width="48" height="14" fill="none" stroke="${color}" />
<text fill="${color}" class="stack-symbol" x="0" y="0" dominant-baseline="central" text-anchor="middle">${alphabet}</text>
</g>`}
).join("") + `
</g>`]
})(),
...g.straight_edges.map(({ label, from, to, config }) => {
return genStraightEdge(
getCenterFromId(g, from),
getCenterFromId(g, to),
label,
from,
to,
config,
);
}),
...g.self_loops.map(({ label, from_eq_to, config }) => {
return genSelfLoopEdge(
getCenterFromId(g, from_eq_to),
label,
from_eq_to,
config,
);
}),
genInitialArrow(getCenterFromId(g, g.initial_vertex)),
...g.vertices.map(genVertex),
...(
() => {
if (!g.pieces) return [];
const pieces = g.pieces;
return pieces.locations.map(loc => {
const center = toScreenCoord(getCenterFromId(g, loc));
const png_content = fs.readFileSync(pieces.piece_path, { encoding: 'base64' });
const bias_x = 148.4 - 124.21;
const bias_y = 63.41 - 32.79;
return `<g>
<image style="overflow:visible;" width="514" height="482" href="data:image/png;base64,${png_content}" transform="matrix(5.581947e-02 0 0 5.581947e-02 ${(center.screen_x - bias_x).toFixed(2)} ${(center.screen_y - bias_y).toFixed(2)})" />
</g>`}
)
}
)()
after(一部抜粋)
export function GraphJSX(g: AutomatonGraph, png_content: string): JSX.Element {
return <g id="graph-root">
<StringTape string_tape={g.string_tape} />
<Stack stack={g.stack} />
{g.straight_edges.map(({ label, from, to, config }) =>
<StraightEdge
src={getCenterFromId(g, from)}
dst={getCenterFromId(g, to)}
label={label}
from={from}
to={to}
config={config}
/>
)}
{g.self_loops.map(({ label, from_eq_to, config }) =>
<SelfLoopEdge
src_eq_dst={getCenterFromId(g, from_eq_to)}
label={label}
from_eq_to={from_eq_to}
config={config}
/>
)}
<InitialArrow arrow_tip={getCenterFromId(g, g.initial_vertex)} />
{g.vertices.map(s => <Vertex state={s} />)}
{g.pieces?.map(loc => <Piece center={toScreenCoord(getCenterFromId(g, loc))} png_content={png_content} />)}
</g>;
}
JSX のおかげで、かなり見通しよくできましたね!
Q & A
Q. Graphviz 使わないの?
A. 「状態数がとても多くて細部まで見てもらう必要がない」といった状態遷移図については使っています。これは「NFA を DFA に変換すると、状態数が指数関数的に爆発することがある」ということを説明するために描画したカージオイド。
一方で、メインの説明に使う図については、SVG でやりとりして頂いたデザインを最大限忠実に反映したいという理由で、この記事で述べたような手法で作られた図を使っています。
-
本人曰く、「Adobe 税を支払いたくない」とのこと ↩