♾️

記法の「軽さ」を最優先した数式マークアップ言語を作った

2023/06/12に公開

はじめに

軽量マークアップ言語(e.g. Markdown)の中で数式を書くという目的においては、TeX記法がデファクトスタンダードになっている。しかし、軽量マークアップ言語自体の「軽さ」と比較したとき、TeX記法は少し「重い」よな〜、ということを前々から思っていた。

ここで「軽い」ことをもう少ししっかり定義しておくと、「プログラムから見た時の扱いやすさや文法としての単純さよりも、人間にとっての見やすさ書きやすさを重視している」ことという感じになるだろうか。[1]

TeX記法もXMLベースのMathMLなどと比べれば圧倒的に軽いのだが、もっと軽くできないものだろうか?特に、僕のようなTeXコードを編集しながら式変形を考えるタイプの人間[2]にとっては、TeX記法でも重すぎると感じることが多い。出てくる記号が多かったり、添え字の添え字のような構造が沢山出てくると、コードは何重にもネストされた括弧とコマンドに埋め尽くされてしまう。こうなると、コードを見て書かれている数式のイメージを咄嗟に掴むことは難しい。

最近はリアルタイムでレンダリング結果が見れるツールが当たり前になってきており、これによってある程度は問題解決できるだろう。しかし、数式を修正したいときにそれがコードのどの部分に対応しているのかを把握するのに時間がかかるという問題は残る。

というわけで、何か良い記法はないのかなあ、とぼんやりと考えていたのだが、ある程度アイデアが出てきたので試しに実装してみた。名前はmaSpaceである。
https://github.com/ho-oto/maSpace
一応デモもブラウザから使えるようになっている。

この記事では、maSpaceのアイデアと実装についてざっくりと説明する。

アイデア

まずは、数式を記述する文法が「重く」なってしまいがちな理由を考察してみよう。すぐに思いつく理由として、以下の2つが考えられる。

  • 数学記号が頻出する
    • アルファベットに無い記号は\alphaのように書くことになるので、表現したい数式に対してコードが長くなりすぎる
    • Unicodeに存在する沢山の数式記号を用いるというのが一つの解決手段だが、これらの記号を快適に入力する環境を作るのは大変[3]
    • AsciiMathのようにアスキーアートを使うという方法もあるが...
  • 数式が深い木構造を持つ
    • こちらがより根の深い問題
    • 木構造をテキスト形式で表現するためには括弧やインデントなどを使う必要があり、コードが括弧まみれになって本来の構造がわかりにくくなってしまう
    • インデントベースの構文を採用するには木が深すぎて「軽く」ならなそう

最近出てきたTypstの数式記法でも、後者については解決できていないように思える。

数式を表現する木構造についてももう少し真面目に考えてみよう。数式の木構造では、累乗^、添え字_、分数/といった中置された演算子が親となり、子としてさらに別の数式が来るか、記号や数字などの終端記号が来るというのが基本と言える(その他にsqrtといった単項演算子もあるがここでは一旦気にしないことにする)。たとえばAsciiMathのASTは

v ::= [A-Za-z] | greek letters | numbers | other constant symbols
u ::= sqrt | text | bb | other unary symbols for font commands
b ::= frac | root | stackrel | other binary symbols
l ::= ( | [ | { | (: | {: | other left brackets
r ::= ) | ] | } | :) | :} | other right brackets
S ::= v | lEr | uS | bSS             ;Simple expression
I ::= S_S | S^S | S_S^S | S          ;Intermediate expression
E ::= IE | I/I                       ;Expression

という感じになっている。

数式の表現に括弧が沢山必要になるのは、こういった演算子の結合順序をコントロールする必要があるからと言える。逆に、これらの演算子の結合の強さをその場でコントロールできれば、括弧を減らし、本来の数式とコードとしての表現を近づけることで、可読性を高めることができるのではないだろうか。

というわけでいよいよ本題だが、maSpaceのアイデアの基本は、^_などの記号の周りにスペースを増やすと、その演算子の結合がその個数の分だけ弱くなる、というものだ。math+spaceでmaSpaceという安直なネーミングである。

たとえばa_b^ca_{b^c}という二つの数式を考えてみる。TeXでa_b^cと書くとa_b^cが表示されるわけだが、a_{b^c}を出すためにはb^cの部分が先に結合して欲しい。そこでTeXではb^cの部分を{}で囲んで結合順序を明示する。maSpaceでは、代わりに_の周りにスペースを入れてa _b^ca _ b^cのようにも書ける(左右にあるスペースのうち多いほうを参照する)。同じようにして、\frac{a_{b_c}^{d^e_g}}{h}のような数式はa _b_c ^d^e_g /hと書ける。

もちろん、全てをこのやり方で表現しようとすると寧ろ分かりにくくなることも多い。なので、括弧を使った明示的な表記と組み合わせることができるようにしておく。たとえばa_{b_c^d}^{e+f_{\frac{g}{h}}}a _b_c^d ^e+f _g/hと書いても良いが、部分的に括弧を使ってa _b_c^d ^[e+f _g/h]と書いたほうが意味が掴みやすいだろう。なお、グループ化のための括弧[]は基本的に表示されず、`[`のようにバッククオートすると表示される仕組みにした。(){}でもグループ化できるが、こちらはデフォルトで表示される。

具体例

READMEから持ってきたものだが、具体例は以下のようになっている。

Result LaTeX AsciiMath maSpace
\frac{a+b}{c} \frac{a+b}{c} (a+b)/c a+b /c (a+b␣/c)
a+\frac{b}{c} a+\frac{b}{c} a+b/c a+b/c
a_{b^c} a_{b^c} a_(b^c) a _b^c (a␣_b^c)
a_b^c a_b^c a_b^c a_b^c
\frac{a_{b_c}^{d^{e+f}_g}}{h} \frac{a_{b_c}^{d^{e+f}_g}}{h} a_[b_c]^[d_g^[e+f]]/h a _b_c ^d ^e+f _g /h (a␣_b_c␣␣^d␣^e+f␣_g␣␣/h)
a _b_c ^d^[e+f]_g /h (a␣_b_c␣^d^[e+f]_g␣/h)
a_{b_c^d}^{e+f_{\frac{g}{h}}} a_{b_c^d}^{e+f_{\frac{g}{h}}} a_[b_c^d]^[e+f_[g/h]] a _b_c^d ^[e+f _g/h] (a␣_b_c^d␣^[e+f␣_g/h])
a _b_c^d ^e+f _g/h (a␣_b_c^d␣␣^e+f␣_g/h)
a_{b_{c^d}}^e+\frac{f_g}{h} a_{b_{c^d}}^e+\frac{f_g}{h} a_[b_[c^d]]^[e]+[f_g]/h a _b _c^d ^e + f_g/h (a␣␣_b␣_c^d␣␣^e␣␣+␣␣f_g/h)
a _b _c^d ^e + f_g/h (a␣␣_b␣_c^d␣␣^e␣+␣f_g/h)
a a a a
<a>
\hat a \hat a hat a â
<'hat>a
<'hat><a>
<a hat>
\alpha' \alpha' alpha' α'
<alpha>'
\not\hat\alpha \not\hat\alpha cancel hat alpha <alpha hat not>
<alpha hat!>
<α hat !>
<α̂!>
<'not><'hat><alpha>
α̸̂
\infty \infry infty <infty>
oo `oo`
\dot\infty \dot\infty dot infty <infty dot>
dot oo <`oo` dot>
<∞ dot>
< < < `<`
\not< \not< cancel < <`<` not>
\sqrt{2} \sqrt{2} sqrt 2 <'sqrt>2
sqrt[2] <'sqrt>[2]
√2
`_/`2
\sqrt[3]{123} \sqrt[3]{123} root 3 123 3 _/ 123
\sqrt{3+4} \sqrt{3+4} sqrt[3+4] √ 3+4
√[3+4]
<'sqrt> 3+4
<'sqrt>[3+4]
\lVert a \rVert \lVert a \rVert norm(a) <'norm>a
`[||` a `||]`
\mathrm{abc} \mathrm{abc} "abc" <"abc" rm>
"abc"
<"abc">
<##"abc"## rm>
\mathbf{ab\\\#"c} \mathbf{ab#"c} <##"ab"#c"## bf>

基本的なアイデアはすでに述べた通りだが、追加で工夫した部分を列挙しておくと

  • TeXで\alphaと書くこところを<alpha>のように書く
    • <alpha dot tilde>のように記号へのアクセントを後ろに書くことができる
    • 基本的にTeXのコマンド名をそのまま使うことにしているので、<aaa>のように書くと\aaaというTeXコードが生成され、レンダリング時にエラーになる。この部分のチェックは(面倒なので)(今のところ)していない。
  • TeXの\dotのような単項演算子は'を使って<'dot>のように書く
  • `oo`のようにバッククオートの中にアスキーアートで表現した数学記号を書くことができる
    • <>を表示したいときもバッククオートに入れる
  • \overset\undersetに対応して^^__が、\sqrt[x]{y}に対応して_/が中置演算子として使える
  • Unicodeの数式記号が使える

あたり。ドキュメントをまるで書いていないので、気になった人は具体例を見ながら適当にデモで遊んでもらうのが良いかなと思う。

実装

基本的にRustで書かれており、maSpaceのコードを受け取って対応するTeXコードを出力するようになっている。あくまで欲しいのは記法の部分なので、レンダリングする部分は既存のエコシステムに乗っかることにする。

なお、後ろではMathJaxで処理することを前提にしている。これは、v3以降のMathJaxであればそのままNode.js上で動くし、ブラウザ無しでSVG出力することもできて便利、というのが理由。Rustで書いたお陰でwasm-packを使ってNode.js用のパッケージが簡単に作れるので、標準出力からmaSpaceのコードを受け取ってレンダリング結果のSVGを返すCLIのようなものも楽に作れた。

処理の流れとしては、nomを使ってトークナイズをした後に、簡単な手書きのパーサーで構文木を作り出力するだけのもの。

デモページはRust+WASMでフロントエンドをするためのライブラリであるyewで最低限動くだけのものを作った。

感想

  • nomは使っていて楽しかった。rust-analyzerも賢いので、コンパイルさえ通れば割と期待通りに動いてくれて気持ちが良い。
  • yewについては、資料が少なく苦労が多かった。これは僕がReactをやったことがないというのも大きそう。
  • 作った言語について、本当に使いやすいものになっているのかはまだ分からない...
    • それを検証するためには処理系が必要だし、自由研究のつもりで作るか〜というのがはじまりだった
    • TeXだとa^b^cのようなものはエラーになるためmaSpaceでも同じようなエラーを出すようにしてあるのだが、スペースを入れまくる関係上この種のエラーが分かりにくいのが気になる
    • 教科書や論文を読んだときのメモの中で個人的に使い込んでみようかなと思う
  • 無駄にUnicodeの数式記号に詳しくなってしまった気がする。ちょいちょいTeXにも無い記号が存在していて不思議
    • ← これがHermitian Conjugate Matrixという名前の記号になっているが、人生で一度も見たことない気がする...
脚注
  1. たとえばWikipediaの軽量マークアップ言語のページを見てみると「データ記述言語としての整合性と、可読性や記述の容易さを両立させたもの」という表現がされている。 ↩︎

  2. なぜこんなことをするかと言うと、自分は字があまりにも汚いので、複雑な式変形をミスなくやるためにはコピペしながらコードを直接弄った方がトータルで早いことがあるためである。 ↩︎

  3. 以前こんな記事を書いたこともあった https://zenn.dev/ho_oto/articles/9fcdfbe143c094 ↩︎

Discussion