手続き型軽量マークアップ言語 gokurai
gokurai は本サイト (inzkyk.xyz) のために自作したマークアップ言語です。前身の gokuro が持つミニマルな設計を受け継ぎつつも、マクロの遅延呼び出しや Lua コードの実行といった機能が追加されています。『コンピューターネットワーク: システム的アプローチ』以降に本サイトで公開されたページ、そして BOOTH で販売された PDF/EPUB は全て gokurai を使って作成されました。
gokurai の実装 gokurai
のソースコードは GitHub にて MIT License で公開されています。
gokurai の特徴
簡単に導入できる
gokurai 用の特別な構文を持たないテキストを gokurai
に入力すると、入力がそのまま出力されます。そのため、途中まで書かれた文章であっても簡単に gokurai を導入できます。
出力を簡単に調整できる
gokurai は組み込みのマクロを最低限しか持たず、ほとんどのマクロを入力ファイル内で定義します。そのため出力を調整するとき処理系の改変がまず必要になりません。また、gokurai
はコマンドラインオプションを持たず、入力から出力が完全に決定します。
Lua が使える
gokurai は Lua コードを実行する機能を持ちます。そのため、変数の読み書きや簡単な文字列の操作から外部プログラムの呼び出しまで様々な処理を行えます。
実装が高速
前身の gokuro と比べて機能が増えたものの、メモリアロケーションの回避などの様々な工夫により gokurai
のパフォーマンスは高速なまま保たれています。特に、マクロの呼び出しの評価順序が遅延評価から積極評価に変わったことで、高度な機能を使わない文書では gokuro より gokurai の方が高速になります。
ブラウザから使える
このページの最初にあるエディタは gokurai の完全な REPL であり、Lua コードの実行を含めた全ての機能が利用できます。自分でビルドしないでも試せます。
使用実績がある
『コンピューターネットワーク: システム的アプローチ』、『オープンソースアプリケーションのパフォーマンス』、『グラフ理論入門』の翻訳で gokurai は使用されました。この文章も gokurai を使って書かれています。
gokurai を使う
このページの最初にあるのは gokurai の完全な REPL であり、「入力」欄にテキストを入力すると gokurai による処理結果が「出力」欄に表示されます。
gokurai の実装は GitHub でも公開されています。この実装は次のコマンドで試せます:
$ git clone https://github.com/inzkyk/gokurai
$ cd gokurai
$ cmake -B build -S . -G Ninja
$ cmake --build build
$ ./build/gokurai hello.gokurai
gokurai の文法
gokurai の文法を例と共に解説します。ここに示す例は編集可能なので、挙動が気になったら「入力」欄を編集して結果を確認してみてください。「元に戻す」ボタンを押すと最初にあったテキストが復元されます。
通常のテキスト
以降で説明される特別な構文を持たないテキストは、全てそのまま出力されます:
マクロの定義と呼び出し
#+MACRO <NAME> <BODY>
の形をした行は名前が <NAME>
で本体が <BODY>
のマクロを定義します。マクロを定義する行は出力されません:
定義したマクロは [[[<NAME>]]]
で呼び出します。マクロの呼び出し [[[<NAME>]]]
を展開すると、名前が <NAME>
のマクロの本体に置き換わります:
定義されていないマクロの呼び出しを展開すると、空文字列となります:
マクロを定義する行にマクロの呼び出しが含まれる場合、その呼び出しを全て展開した結果が定義されるマクロの本体となります。そのため、マクロを定義する順番が意味を持ちます:
ローカルマクロ
#+LOCAL_MACRO <NAME> <BODY>
の形をした行はローカルマクロを定義します。ローカルマクロは定義された次の行でのみ有効になります:
同じ名前を持つ通常の (グローバルな) マクロとローカルマクロが存在する場合、ローカルマクロが優先されます:
なお、ローカルマクロの定義の後に別のローカルマクロの定義が続いた場合、さらにその次の行では両方のローカルマクロが有効になります。つまり、正確に言えばローカルマクロが削除されるのは「次の行」ではなく「次のローカルマクロの定義でない行」です:
マクロの遅延呼び出し
マクロの呼び出しの先頭に ^
を付けて ^[[[<NAME>]]]
とした場合、その呼び出しは遅延呼び出しとなります。その名の通り、遅延呼び出しは他の処理より後に展開されます。
例えば、マクロを定義する行にマクロの遅延呼び出しが含まれる場合、その遅延呼び出しは定義されるマクロの本体に保存され、マクロを呼び出したときに展開されます。そのため、そういったマクロの展開結果は、呼び出した時点で定義されているマクロによって変化します:
マクロの引数
マクロには [[[<NAME>(<ARG1>,<ARG2>,...)]]]
の形で引数を渡せます。こうすると、マクロの本体に含まれる $1
, $2
, ... がそれぞれ <ARG1>
, <ARG2>
に置き換わります:
特別な記法として、$0
は引数を囲む括弧の間にある文字列そのものに置き換わります。コンマを無視したい場合に便利です:
組み込みのマクロ
組み込みの特別なマクロがいくつか定義されています:
__LUA__
__LUA__
マクロの第一引数は Lua コードとして実行され、返り値を文字列に変換したものがマクロの展開結果となります。Lua コードの返り値とは return
された値です。返り値の文字列への変換では lua_tolstring
が使われます:
最初の return
は省略できます:
Lua の実行環境は処理の最後まで同一のものが使われます。そのため、定義した変数などは最後まで利用できます:
__NO_NEWLINE__
__NO_NEWLINE__
マクロの呼び出しが行末にあると、その呼び出しの直後の改行が存在しないものとして扱われます:
__INPUT_LINE_NUMBER__
, __INPUT_LINE_NUMBER__
__INPUT_LINE_NUMBER__
マクロは現在の入力行番号に展開され、__INPUT_LINE_NUMBER__
マクロは現在の出力行番号に展開されます。一般に両者は一致しません。
__DISABLE_LUA__
, __ENABLE_LUA__
Lua コードの実行を無効化/有効化します。実行が無効のとき、__LUA__
マクロの展開結果は常に空文字列となります。初期状態で実行は有効です。
エスケープとクオート
マクロの引数にコンマが含まれる場合は、\
でエスケープします。コンマの直前に \
がある場合は、それも \
でエスケープできます:
gokurai で使われる記号列 [[[
や ]]]
を出力するには、'
でクオートします:
#+MACRO <NAME> <BODY>
なども同様に '
でクオートします:
コメント
#+COMMENT_BEGIN
...#+COMMENT_END
で囲まれた部分はコメントとなり、出力されません:
複数行のマクロ
複数行のマクロと定義するための記法が用意されています:
ローカルマクロにも同様の記法があります:
gokurai の使用例
機能を羅列しただけでは gokurai を実際にどう使うかが分からないと思うので、gokurai の機能が活かされる使用例をいくつか示します。
HTML 要素に属性を付ける
ローカルマクロは「ほとんどの場合で空文字列だが、たまに空文字列でなくなる」部分に利用すると便利です。例えば、HTML 要素の属性 (クラスやスタイルなど) はローカルマクロを使うと簡潔に表現できます:
章番号を自動で振る
Lua コードの呼び出しとマクロの遅延呼び出しを使えば、自身が呼び出された回数を記憶する「カウンター」付きのマクロを定義できます。
例えば、次に示すのは「カウンター」が付いた chapter
マクロです:
chapter
マクロの定義にある [[[__LUA__(x = 0;)]]]
はマクロの定義時に展開され、^[[[__LUA__(x = x + 1; return x;)]]]
はマクロの呼び出し時に展開されることに注意してください。
Lua コードには任意の処理を書けるので、「前書きには番号を付けない」「第 0 章から始める」「章番号をローマ数字にする」といった特殊な状況にも簡単に対応できます
HTML と LaTeX の両方にエクスポートできる文書を書く
gokurai は未定義のマクロの呼び出しを削除します。そのため「マクロの定義をいくつか書いて、一つだけ選択する」使い方ができます。
これを使うと、HTML や LaTeX といった複数のフォーマットにエクスポートできる文書を書くことができます:
#+MACRO chapter [[[html(<h1>$0</h1>)]]][[[latex(\chapter{$0})]]]
#+MACRO code [[[html(<code>$0</code>)]]][[[latex(\texttt{$0})]]]
[[[chapter(基本的なコマンド)]]]
本章では [[[code(ls)]]] や [[[code(cd)]]] といった基本的なコマンドを説明します。
この文書の最初に #+MACRO html $0
または #+MACRO latex $0
を付ければ、それぞれ HTML と LaTeX のソースコードが得られます:
もちろん、同じ方法で HTML と LaTeX 以外のフォーマットにも対応できます。新しいフォーマットのサポートを追加するときに必要なのは、執筆している文書で定義されるマクロに対する新しい定義だけです。膨大なドキュメントを読み漁ったり、慣れない言語と格闘する必要はありません。
マクロの定義に含まれる未定義のマクロの呼び出しは定義時に展開 (削除) されるので、サポートするフォーマットを増やしても gokurai
の処理時間はほとんど増えないことにも注目してください。
情報をファイルに書き出して、外部プログラムで処理する
Lua にはファイル IO と os.execute
があるので、それらを組み合わせれば「入力から抽出した情報を Lua 以外の言語で処理して、その結果を出力のテキストに埋め込む」処理が書けます。
Lua はそれほど強力な言語ではないので、複雑な処理は他の言語に任せた方がいいでしょう。このテクニックは『コンピューターネットワーク: システム的アプローチ』と『グラフ理論入門』で索引を作るときに利用されました。
なお、上の例で foods.txt
に含まれる食べ物の順番が逆転している理由については 心残りな点: Lua コードの実行順序がおかしい で説明しています。
余談: ブラウザ上でファイルの読み書きができている (ように見える) のは gokurai
が特別な処理をしているからではなく、コンパイラの emscripten がブラウザ向けの仮想ファイルシステムを実装しているためです。
その他
以上で gokurai の紹介は終わりです。以降では gokurai を設計・実装するときに考えたことや、せっかくなので書いておきたいことなどを書きます。
gokuro からの主な変更点
マクロを呼び出す構文の変更
gokuro ではマクロを <<<NAME>>>
と呼び出すのに対して、gokurai では [[[NAME]]]
と呼び出します。<<<NAME>>>
の利用を止めた理由は次の通りです:
-
衝突する: Julia や Java が持つ論理右シフト演算子は
>>>
で表されます。また、C++ で複雑なテンプレートを書くと>>>
が現れます。これに対して、gokurai が使う[[[
と]]]
が gokurai 以外の文書で現れることはまずないです。 -
括弧の対応が容易に崩れる: 数学の不等号や C/C++ の
->
演算子として<
や>
が単体で使われると、文書全体に含まれる<
と>
の個数が一致しなくなります。こうなるとテキストの編集に支障が出る上に、機械的に検知できるミスが検知できなくなります。これに対して、[
と]
が単体で使われることはまずないです。 -
HTML のタグと混同する: gokuro で HTML を含む複雑なマクロを書くと、
<
と>
が入り乱れて解読が困難になります。また、名前がfoo
で本体がFOO
であるマクロを gokuro で呼び出そうとしたときに誤って<<<<foo>>>>
と書いてしまうと、気が付きにくい問題が起きます: 展開後の文字列は<FOO>
となりますが、ブラウザはこの文字列を (無効な) HTML タグと認識するので文字としてレンダリングしません。
これほど問題のある構文を gokuro で採用したのか不思議なくらいです。
クオートの追加
gokuro は <<<
や >>>
などの特別な構文をクオートする方法を持たないので、解説を書くときに苦労しました。gokurai はクオートを持つので、この解説も通常の記事と同様に、特別な処理をすることなく書けています。
マクロの評価順序の変更
gokuro ではマクロの定義に含まれるマクロの呼び出しは必ず遅延評価 (定義されるマクロの呼び出し時に展開) されます。これに対して gokurai では、マクロの定義に含まれる通常のマクロの呼び出しは積極評価 (マクロの定義時に展開) され、遅延呼び出しだけが遅延評価されます。
このため、gokurai ではマクロの定義に含まれる (通常の) マクロの呼び出しが一度だけ展開されます。gokuro ではマクロの呼び出しを含むマクロを呼び出すたびに定義に含まれるマクロが展開されるので、gokuro と比べたとき gokurai ではマクロを展開する回数が大きく減少します。
"HTML と LaTeX の両方にエクスポートできる文書を書く" で触れたように、gokurai では新しいフォーマットのサポートを追加しても処理時間がほとんど増えません。これに対して、gokuro では処理時間がサポートするフォーマットの数だけ増えます。
複数行のマクロを書ける構文の追加
gokurai では #+MACRO_BEGIN <NAME> ... #+MACRO_END
で本体に改行を持つ複数行のマクロを定義できます。gokuro の解説では「改行を入れたいなら、適当なマークを入れて後から sed
で置換しろ」と苦しい言い訳をしていますが、複数行のマクロが書けた方が当然便利です。
この機能の実装にはそれなりに手間がかかりました。gokurai は入力を行ごとに処理するので、現在処理している行が複数行になった場合には行末をもう一度検索し、さらに次の入力行の読み込みでは本来の入力ではなく新しい行末の次の文字から読み込む必要があります。バグもいくつか生まれました。
Lua コードの実行機能の追加
gokuro を使って文章を書く中で「変数や辞書を使った簡単なプログラムを書きたい」と感じることが何度かありました。例えば現在の章のタイトルを取得する処理はマクロを使っても書けますが、プログラムとして書いた方が簡潔で効率的です (マクロの定義は上書きできるだけで削除はできないため)。プログラミング言語の構文のように振る舞う組み込みマクロ (例えば __SET__(<NAME>,<VAL>)
や __IF__(<COND>)
など) を追加することも考えましたが、手間がかかりすぎると思ったので Lua を使うことにしました。非常に簡単に組み込めて特に問題も起こらず、自分の想像通りのことができるようになったので、Lua を選んだのは正解でした。自分でプログラミング言語 (もどき) を実装していたらと思うと寒気がします。
Lua の組み込みが上手く行ったのは、Lua と gokurai 間のやり取りがほとんど必要ないことが大きいと思います。自分が gokurai を使う上で Lua に求められるのは引数と Lua の変数から文字列を組み立てて返すことであり、gokurai で定義されたマクロの情報にアクセスして何かすることではありません。gokurai と Lua で相互に情報のやり取りが必要だとしたら、組み込みはもっと面倒になっていたでしょう。
また、「理由は知らないが、ここに改行があってはいけないと Markdown パーサーが言っている」状況に対応するために __NO_NEWLINE__
マクロ
-
#+MACRO_BEGIN <NAME> ... #+MACRO_END
によるコメント機能が内蔵された。 -
__LUA__
,__NO_NEWLINE__
といった組み込みのマクロが追加された。
心残りな点
入力を行ごとに読み込んでいない
gokurai は入力を行ごとに処理します。そのため入力を (バッファしつつ) 行ごとに読み込んで処理する実装が可能ですが、そうなっていません。gokurai は入力を全てバッファに読み込んでから処理するので、少しずつ書き込まれるログファイルやメモリに載り切らないほど巨大ファイルを扱えません。
こうなっている理由は...実装が面倒だからです。私が gokurai を使うときの入力はせいぜい数 MB のテキストファイルなので、実装を複雑にしてまで行ごとの処理を実装する必要はないと判断しました。
ちなみに、出力をバッファして適当な量になったら書き出す処理は実装されています。簡単に実装できるので...。
Lua コードの実行順序がおかしい
gokurai は「現在の行にある最も後方のマクロを展開する」処理をマクロが見つからなくなるまで繰り返すことでマクロを展開します。そのため、副作用を持つマクロが同じ行に複数あると、最も後方のものから実行されます:
この直感的でない展開順序は gokuro から受け継いだものです。gokuro では任意のマクロが副作用を持たないのでマクロの展開順序が問題にならず、最も後方のものから展開すると入れ子になった呼び出しが自動的に正しく処理されるので実装が楽でした。
副作用を持つマクロが書けるようになった時点で展開順序を直すべきでしたが、マクロの展開処理が複雑なので手を出せていません。また、少なくとも私の書く文章では「同じ行に副作用を持つマクロが複数ある」状況が基本的に発生しないので、直すモチベーションがあまり高くありません。
ローカルマクロのスコープがおかしい
複数行のマクロの本体でローカルマクロを定義すると、その定義はマクロ本体を処理している間は削除されず、次の入力行を読むまで残ります:
macro
マクロに含まれる二つ目の ^[[[foo]]]
は空文字列に展開されるのが自然です。
ローカルマクロを削除するタイミングを「次の入力行を読むとき」ではなく「新しい行の処理を始めるとき」にすればいいじゃないか、と思うかもしれません。しかし、そうすると次の例で出力が変わります:
つまり、ローカルマクロの削除タイミングを「次の入力行を読むとき」にするとローカルマクロの定義が正しく動かなくなり、「新しい行の処理を始めるとき」にするとローカルマクロの呼び出しが正しく動かなくなります。複数行のマクロの本体でローカルマクロの定義とローカルマクロの呼び出しのどちらが多く現れるかと言えば (当然) 後者なので、そちらが正しくなるように実装しました。ローカルマクロの解説で「正確に言えばローカルマクロが削除されるのは『次の行』ではなく『次のローカルマクロの定義でない行』です」と書きましたが、さらに正確に言えば「ローカルマクロが削除されるのは次のローカルマクロの定義でない入力の行」です。
全てのローカルマクロを「正しい」位置で削除する実装は...面倒なのでやめました。
機能が多すぎる
使いながら薄々感じていましたが、この記事で全ての機能の解説を書いてみて、機能の多さを改めて実感しました。gokurai を使いこなすには、少なくとも次の要素を理解しなければなりません:
- マクロの定義
- ブロック形式のマクロの定義
- マクロの (遅延) 呼び出し
- マクロとブロックの評価タイミング
- マクロの展開順序
- Lua の挙動
__NO_NEWLINE__
マクロの挙動
もちろん、こういった要素を意識しないで文書を書けるようにマクロを定義するのですが、マクロのデバッグは骨が折れます。開発者の自分でも何が起きているのかすぐに分からないことがあります。
実装が複雑すぎる
機能が多くなったので、実装も複雑になりました。メイン処理の実装は 1500 行程度で、ほとんどの部分が一つのクラスに含まれます。単純だった gokuro のソースと比べて一気に複雑になりました。gokuro は週末のプロジェクトとしてちょうどいいぐらいの規模でしたが、gokurai を週末で実装するのは無理でしょう。
機能の多さから生じるバグも結構ありました。gokurai.cpp
の下の方を見ると確認できます。
エラー/警告が無い
引数を取るマクロに誤った個数の引数を渡したり、引数を渡さないで呼び出したとしても警告やエラーは出ません。面倒なので実装しませんでした。
マクロに仮引数が無い
マクロの引数は必ず $0
, $1
, ... で参照しますが、マクロの引数に名前を付けられると読みやすい気がします:
#+MACRO link(url,text) <a href="[[[url]]]">[[[text]]]</a>
面倒なので実装しませんでした。この機能の実装はかなり手間がかかると思います。
Lua コードを自動的に実行できない
gokurai で Lua コードを実行するには、組み込みの __LUA__
マクロの呼び出しまたは #+LUA_BEGIN ... #+LUA_END
のブロックを書く必要があります。そのため、「ある条件が満たされたとき Lua コードを自動的に実行する」処理は書けません。例えば、段落が始まるとき何らかの Lua コードを実行したいなら、文書に含まれる全ての段落の先頭に何らかの文字列を書く必要があります。これは面倒です。
この解決策として、何らかの「フック」を提供する方法が考えられます。次のようなフックが考えられるでしょう:
before_gokurai_hook(input)
: gokurai への入力を読み込んで処理を始める前に呼ばれる。after_gokurai_hook(output)
: gokurai の出力を書き出す処理を始める前に呼ばれる。before_each_line_hook(line)
: 各行の処理を始める前に呼ばれる。after_each_line_hook(line)
: 各行の処理が終わったときに呼ばれる。macro_call_hook(name, args...)
: 処理すべきマクロの呼び出しを見つけたときに呼ばれる。undefined_macro_call_hook(name, args...)
: 未定義のマクロの呼び出しを見つけたときに呼ばれる。
こういったフックは [[[__SET_BEFORE_EACH_LINE_HOOK__(lua_function)]]]
などとして設定できるはずです。
フックを実装していない理由は...今まで思いつかなかったからです。この解説を書く中で有用性がはっきりしたので、そのうち実装するかもしれません。
マクロの呼び出しを検索する正規表現を書くのが面倒
[
と ]
は正規表現のメタ文字なので、検索するときにエスケープが必要です。そのためマクロの呼び出しにマッチする正規表現は \[\[\[.*+\]\]\]
です。
面倒だなとは思いましたが、この問題を解決できて他の問題を起こさない記号を思いつかなかったのでそのままにしました。
WebAssembly バイナリが大きい
gokurai を動かすための WebAssembly バイナリと JavaScript コードは合わせて約 410 KB です。Lua の機能を全て含んでいるので仕方ない面もありますが、あまり「軽量」とは言えません。気合いを入れればもっと小さくなるかもしれませんが、Web 上の REPL はオマケみたいなものなので気にしないことに決めました。未圧縮の画像をダウンロードとしたと思ってください。
ちなみに Lua の機能を全て省くとサイズは約 43 KB まで小さくなるので、gokurai
のバイナリの 9 割近くは Lua 関連のコードです。
fuzzer に対応していない
文字列を受け取って文字列を返す gokurai
は fuzzer にピッタリなので使って見たいな~とずっと思っていましたが、ついぞ使うことはありませんでした。そのうちやるかも。
複数行のマクロの処理が遅い
複数行のマクロはあまり気合いを入れて実装されていません。入力のテキストから数行のマクロをたまに呼び出すだけなら問題ありませんが、複数行のマクロが入れ子になると無駄な処理 (「外側」のテキストを全て移動させる処理) が生じます。外側のマクロが長いほど無駄な処理は増えるので、例えば次のような文書は書くべきではありません:
#+MACRO include <ファイル $1 を読み込んで、その内容を返す>
[[[include(chapter_1.txt)]]]
[[[include(chapter_2.txt)]]]
[[[include(chapter_3.txt)]]]
...
それぞれのテキストファイルが大きく、かつ複数行のマクロの呼び出しが多く含まれる場合、gokurai の処理は非常に遅くなります。インクルードするファイルの内容を直接書くか、別のプログラムでインクルードを処理した方がいいでしょう。
この問題を解決するには手の込んだデータ構造が必要になると思います。面倒なので実装する予定はありません。
上手く行った点
動いた
gokurai
は (一応) 完成し、ウェブページや PDF を出力するのに実際に利用されています。これ以上に重要なことはないです。
ix
ライブラリを使って実用的なプログラムが書けた
ix
は C++ の練習がてら数年前からチマチマ作っていた自作のライブラリです。gokurai
は ix
を使って書いた初めての非自明なプログラムでした。
ix
には粗削りな部分もありますが、ix
の開発中に得られたノウハウや構築されたインフラは gokurai
の開発で役立ちました。もちろん ix
自体も様々な箇所で利用されました。
テストが書けた
gokurai
は doctest
というライブラリで書かかれたテストを持ちます。gokurai
の開発は最初に gokuro
を実装し、そこから機能を追加・変更することで進んでいったので、テストは大きな価値を持ちました。いつか gokurai に機能を追加することがあれば、そのときも後方互換性を担保する上でテストが役立つでしょう。
doctest
は ix
でも使っていました。ix
でテストのために書いた補助クラス ix_TempFileR
と ix_TempFileW
は gokurai のテスト (特に CLI のテスト) でも役立ちました。
また、コンピューターネットワーク: システム的アプローチ は最初 gokuro で書き、一度最後まで書ききってから gokurai に移植しました。こうすることで一冊の本が丸ごとデバッグ用テキストとして機能し、設計の考慮漏れがいくつか判明しました。
「文字列を文字列に変換するプログラムでテストを書くなんて当たり前だろ!」と言われればその通りですが...。
C++ のツールを活用できた
ix
と gokurai
の開発では様々な C++ のツールを使いました:
- CMake
clang
/gcc
/cl
の警告- address sanitizer
valgrind
clang-tidy
cppcheck
llvm-cov
/llvm-profdata
- MSVC のデバッガ
これらのツールを簡単に呼び出すための python スクリプトも書きました。このスクリプトは ix
の開発を始めたときから少しずつ書いてきて、かなり便利になったと思います。
Web ブラウザから実行可能にできた
元々 ix
と gokurai
はどちらも WebAssembly にコンパイルできるように書かれていませんでした。しかし、この解説を書くにあたってせっかくなので WebAssembly に対応させ、ウェブブラウザ用の REPL を作りました。多少手間がかかりましたが、コンパイラの emscripten が非常に優秀だったので思っていたよりも簡単にできました。
入力が文字列を表現するようにした
gokurai では入力の文字列が出力の文字列を表現します。これは通常のプログラミング言語 (コンピューターが実行する処理を表現する) やマークアップ言語 (木構造を表現する) と根本的に異なります。こうしたおかげで実装は非常に簡単になりました。このアイデアが gokurai で上手く行った理由としては次の点を指摘できます:
-
出力の構築だけを考えれば済む: プログラム言語では表現された処理を最適化したり、マークアップ言語では表現された木構造を処理しやすい形式でメモリ上に格納したりします。これに対して、言語が文字列を表現するとき、表現された (構造を持たない) 文字列に対してできる処理はありません。このため表現された文字列の計算以外に考えるべきことが無くなります。
-
入力と出力がそれほど変わらない: 例えばコンパイラの入力と出力は大違いですが、gokurai の入力と出力には一致する文字列が多く含まれます。
この「文字列を表現する言語」というアイデアは筋がいいと思うので、ちゃんとした人 (?) にちゃんとした言語を作ってほしいです。
想定される質問への解答
Q. どうして gokurai を作ったのですか?
A. gokuro の機能の少なさに不便を感じることが増えたためです。
ただ、これはあくまでも "表" の理由で、"裏" の理由として「翻訳していた『コンピューターネットワーク: システム的アプローチ』があまりにも長すぎて途中で飽きてきたので、気晴らしに作った」があります。こうしたことで翻訳したての文章を gokurai 用のデバッグテキストとして使えるようになったのはラッキーでした。
Q. gokurai という名前の由来はなんですか?
A. 前身である gokuro のもじりです。特に意味はありません。テキストゆれないくんの CLI プログラム yurenai
(非公開) と似た名前にしました。
トリビア: gokurai は元々 "gokuraku" と呼ばれていましたが、「外国人が雑に日本語の名前を付けたプロダクトみたい」「このプログラムを使ったところで文章を書くのは決して gokuraku ではない」「宗教用語では?」などの理由から改名されました。
Q. gokurai を作る意味はありましたか?
A. gokuro を使っていたときと比べて原稿ファイルがずっと書きやすくなったので、作った意味はあったと思います。少なくとも、一年近く gokurai を使う中で gokuro に戻りたいと思ったことはありません。
特に、gokurai には gokuro には無かった安心感があります: チューリング完全なプログラミング言語 Lua が搭載されているので、最悪の場合でも Lua コードを書けば (または Lua コードから外部プログラムを呼べば) たいていのことはできるからです。gokuro を使うときは「gokuro で書けない処理が必要にならないだろうか? もしそうなったら python を書かなくては...」という不安が常にありました。
Q. 「手続き型軽量マークアップ言語」ってなに?
A. プログラミング言語の "カテゴリ名" はほとんど何も意味していないことが多いので、あまり気にしないでください。一応、「手続き型軽量マークアップ言語」には次の意味があります:
-
「手続き型」
- 手続き型プログラミング言語の Lua を書ける。
- マクロの展開で AST が構築されず、呼び出しが「一つずつ」展開される。
-
「軽量マークアップ言語」
- 軽量マークアップ言語と呼ばれる Markdown と同じような使い方ができる。
- 使い始めるときに理解する必要のある機能が少ない。
Q. Lua なんて誰も書いてないよ!
A. 確かに Lua は知名度が低く、変な機能も少なからずあります (1
始まりの添え字など)。gokurai で Lua を採用した理由は次の通りです:
-
ビルドが簡単: ソースコードをダウンロードしてプロジェクトに加えればそれだけでビルドできます。CMake とか git とか
configure
とか依存ライブラリとか DLL とかパッケージマネージャとかを意識しないで済みます。 -
実績と歴史がある: ゲームのスクリプトやソフトウェアのプラグイン用の言語として Lua は実際に使われています。また、Lua には 20 年以上の歴史があります。
-
機能が十分: 具体的に言えば、基本的な変数操作と制御構文、ファーストクラスの関数と辞書、ファイル IO、
os.execute
、luaL_loadbuffer
とlua_pcall
があります。 -
構文が自然: C に似ているので、例えば Haskell よりは理解しようという気になる人が多いでしょう。
-
触れたことがある: Lua のマニュアルを翻訳したことがあったので、Lua の特徴や組み込み方法は大まかに知っていました。
余談: 「Lua の方が Haskell よりユーザーが多い」と書こうと思って TIOBE Index を確認したら、Haskell が 31 位、Lua が 32 位に仲良く並んでました。Haskell の方が使われているじゃないか!
Q. REPL に無限ループする文書を入力したらブラウザごと固まったのですが
A. 仕様です。頑張ってタブを閉じてください。
この文書を REPL に入力するな!!!!!!
#+MACRO foo ^[[[foo]]]
[[[foo]]]
Q. gokurai にバグはありますか?
A. ほぼ間違いなくあります。gokurai を使う中でバグをいくつも潰してきましたが、全て潰せたとはとても考えられません。この記事を書いている間にもバグが二つ発見されました。
Q. gokurai をアップデートする予定はありますか?
A. ありません。gokurai は「完成」していると思ってください。バグフィックスもしません。
仮に機能を追加するとしても、数年単位で時間を置いてからだと思います。参考: gokuro から gokurai まで 4 年以上かかりました。
Q. 最終的な翻訳の質がね...。
A. すいません...。精進します......。
ただ、これだけは言えます: もし gokurai が無かったら、もっとひどい翻訳が世に出ていたでしょう。
Q. こんな長い文章を書く暇があったら新しい機能の一つや二つ実装できたのでは?
A. 確かに...。
まとめ
- gokurai は自作の手続き型軽量マークアップ言語です。
- このサイト (inzkyk.xyz) は gokurai を使って作られています。
- gokuro をベースにマクロの遅延呼び出しや Lua コードの実行といった機能が追加したのが gokurai です。
- gokurai のソースコードは GitHub で公開されています。
- よろしければお使いください。