第2回: Projectional Editing — もう一つのやり方
シリーズ「構文エラーのない世界へ」 — 目次に戻る / 前回
パーサーを消す
前回、構文エラーが生まれる根本原因を見た。プログラマーがテキストを書き、パーサーがそれをASTに変換する。この変換が途中状態で失敗する。
Projectional Editing は、この問題を根本から消す。
【従来】
プログラマー → テキスト → パーサー → AST → コンパイラ
【Projectional Editing】
プログラマー → AST を直接操作 → 射影 → 画面に表示
プログラムの「本体」はテキストファイルではなく、ASTそのものだ。画面に表示されるテキスト風の見た目は、ASTを人間向けに変換(射影 = projection)した結果にすぎない。
パーサーが消えた。パーサーがなければ、構文エラーも発生しない。
「穴」で未完成を表現する
しかし、一つ疑問が生まれる。プログラムを書いている途中、「まだここは決めていない」をどう表現するのか?
答えは 穴(Hole) だ。
let x = _ in x + _
_ が穴を表す。「ここにはまだ式が入っていないが、式が来るべき場所だ」という意味だ。
穴はASTの中で正式な市民権を持つ。普通のノード(数値、変数、関数)と同じように、ASTの一部として存在する。
let
/ | \
x _ + ← 穴が正規のノードとして存在
/ \
x _
このASTは不完全だが、壊れてはいない。木の構造は整合している。だから型チェックもできる。
let x = _ in x + _
↑ ↑
"Int型の式を "Int型の式を
入れてね" 入れてね"
型チェッカーは穴を見つけると、「ここにはこの型の式が必要です」という情報を計算して返すことができる。入力の途中でも型情報が使える。これはテキストベースのエディタでは原理的に難しいことだ。
ミシガン大学の Hazel プロジェクトは、この「穴のあるプログラムにも完全に意味を与える」という考え方を、数学的に証明した形式体系として発表している(Omar et al., POPL 2017)。穴があっても型チェックできるだけでなく、穴がある状態でプログラムを実行すらできる。穴に到達すると、「ここは未定だが、周辺の計算はここまで進んだ」という情報が得られる。
Projectional Editing の良さ
構文エラーからの解放
最も直接的なメリット。ASTを直接編集する以上、パーサーが失敗する余地がない。初学者がプログラミングを学ぶとき、構文エラーの解読に費やす時間は膨大だ。Projectional Editing はその負担を根本から取り除く。
複数の表現を自由に切り替え
ASTが「本体」なので、それを表示する方法は一つに限らない。
同じプログラムを:
- テキスト風に表示する(従来のコードの見た目)
- ブロックとして表示する(Scratchのようなビジュアルプログラミング)
- 数式として表示する(行列を2D表記で)
- 図表として表示する(状態遷移図をそのまま)
ASTは同じ。見た目だけが変わる。これが「射影(projection)」の意味だ。
JetBrains社の MPS (Meta Programming System) はこの考えを実用化した代表的なシステムで、mbeddr という組み込みC言語の拡張環境では、状態遷移図やデシジョンテーブルをコードと同じファイル内に直接埋め込める。
言語合成が容易
従来、二つの言語を混ぜて使おうとすると、文法が衝突する。HTMLの中にJavaScriptがあり、その中にJSXがあり……パーサーを書く人の悪夢だ。
Projectional Editing ではパーサーがないので、異なるDSL(ドメイン特化言語)を自由に組み合わせられる。SQL風のクエリ、数式、正規表現、それぞれ最適な見た目でコード内に埋め込める。
リアルタイムの型情報
穴のあるプログラムにも型がつくので、編集のどの瞬間でも型情報が利用できる。「この穴には String 型の式を入れてください」「この式は期待される型と矛盾しています」といったフィードバックが途切れない。
Projectional Editing の難点
良いことばかりではない。実際に使ってみると、深刻な問題が浮かび上がる。Voelter et al. (FSE 2016) は MPS ユーザーを対象とした実測研究で、以下の課題を確認している。
Viscosity(粘性)問題
簡単に思える変更に、意外と多くのステップが必要になる。
テキストエディタで 1 + 2 を (1 + 2) * 3 に変えたいとする。テキストなら、先頭に ( を打ち、末尾に ) * 3 を足すだけ。
Projectional Editor では:
+ノードを選択する- 「式で包む(wrap in expression)」アクションを実行する
- 包む演算子として
*を選ぶ - 新しい
*ノードの右辺の穴に3を入力する
操作の粒度がASTノード単位なので、テキスト的な直感と噛み合わない場面が多い。特に、式の構造を組み替えるタイプの編集(演算子の優先度を変える、関数の引数をまるごと別の場所に移す、等)で顕著になる。
テキスト入力の自然さの喪失
プログラマーにとって「テキストを打ち込む」行為は、思考をそのまま流し込むプロセスだ。
"関数を書こう..." → fn と入力し始める
"あ、やっぱり let" → バックスペースで消して let と打つ
"名前は... x" → let x
"型は Int" → let x : Int =
"値は..." → ここで考える
文字列はこの間ずっと「パースできない状態」を経由するが、プログラマーにとってはごく自然な流れだ。
Pure Projectional Editor ではこれが「穴をクリック → メニューから let を選択 → 名前の穴をクリック → x と入力 → 型の穴をクリック → Int を選択 → 値の穴をクリック → …」というフォーム入力になる。思考の流れが、操作の手順に分断される。
「壊してから直す」ができない
ベテランプログラマーは、リファクタリング時に「一旦コードを壊して、大きく組み替えてから、また整合性を取る」ということを頻繁にやる。テキストエディタなら、コードが一時的に壊れた状態でも、自由にテキストを動かせる。
Projectional Editor ではASTが常に整合していなければならない。コピー&ペーストも、ASTノード単位でしか行えない。テキストの断片を「とりあえずここに置いておく」ができない。
外部ツールとの非互換
git diff、grep、sed、vim。プログラマーのワークフローはテキストベースのツールの上に成り立っている。Projectional Editor のデータ形式は独自のASTであり、これらのツールとの互換性がない。チーム開発で diff のレビューをどうするか、という問題一つとっても重大だ。
まとめ: 美しいが、まだ足りない
Projectional Editing のアイデアは本質的に正しい。「プログラムは木であり、木を直接編集すべきだ」という原則に反論の余地はない。構文エラーのない世界は実現可能で、穴を使えば未完成のプログラムにも意味を与えられる。
しかし、テキスト入力の自然さを犠牲にしてしまう。これは採用の致命的な障壁になる。プログラマーは何十年もテキストで考え、テキストで書いてきた。その身体感覚を無視したツールは、どんなに理論的に優れていても使われない。
では、テキスト入力の自然さと、構造編集の安全性を両立させることはできないだろうか?
次回は、この問いに取り組む Hazel プロジェクトの「Gradual Structure Editing」を見ていく。