プログラミング
実験 – プロジェクショナルビューア
Experiment – Projectional Viewer (programmingsimplicity.substack.com)
要約
本実験は、現代のプログラミング言語が実装の詳細に構造を埋もれさせているという問題提起から始まります。投稿者は、コードから実装の詳細を隠蔽する「プロジェクショナルビューア」の可能性を探求し、コードの構造をより明確に理解できるエディタの実現を目指しています。この実験では、Rebolで書かれた電話番号の文法例を使い、Emacs LispとOhmJS、RWRを組み合わせたパイプラインで実装の詳細を非表示にする試みを行い、その有効性を検証しています。
全文翻訳
実験 — プロジェクショナルビューア
2026-08-26
Paul Tarvydas
2026年8月26日
3
共有
目的
現代のプログラミング言語は、構造を実装の詳細の下に埋もれさせています。信号対雑音比が低すぎます — 「何」を「どのように」で覆い尽くしており、かなり単純な問題を超えて何かを推論するのが困難になっています。
この実験では、実装の詳細を削除するのではなく、コードから隠蔽できるかどうかを問います。詳細はソースに残りますが、エディタでは表示が切り替えられます。目標は、その構造がどのように実行されるかのメカニズムを掘り下げることなく、ソリューションの構造をより明確に把握することです。
直接的な不満はPEG(Parsing Expression Grammar)ライブラリでした。私は文法をかなりよく理解していますが、私が使用したほとんどのPEG実装は、パース中およびパース後にマッチをキャプチャしてアクションを実行するためのコードの層の下に文法自体を散らばせています。文法 — 認識されているものの実際の構造 — は配管の中に消えてしまいます。
編集可能な完全なラウンドトリッププロジェクショナルエディタをうまく構築するのは困難です。そのため、この実験では片道の旅に落ち着きます:編集可能な形式のサポートを試みることなく、読み取りのために詳細を隠蔽します。問題は、そのより低い目標が容易に達成可能であり、それでも価値があるかどうかです。
これの根底にあるのは、「文法」をパターンマッチングのためのDSL(Domain-Specific Language)として扱う方法です — 別の言語に埋め込まれた小さな言語。もし隠蔽がここで機能するなら、それはより広範な道を示唆します:すべての懸念を1つの汎用パラダイムに押し付けるのではなく、多くの小さな特殊目的言語を単一のコードベースに構成することです。
方法
私は小さな実際の例から始めました:Rebolで書かれた電話番号の文法、実装の詳細も含めて。
parse "(416)555-1234" [ "(" copy area 3 digits ")" copy exch 3 digits "-" copy line 4 digits (print rejoin [area "-" exch "-" line]) ]
copy area、copy exch、copy line、および末尾の(print rejoin ...)ブロックはすべて実装の詳細です — それらは値をキャプチャしてそれらに対して何かを実行することに関するものであり、有効な電話番号の形状に関するものではありません。それを剥ぎ取ると、文法自体は次のようになります。
parse "(416)555-1234" [ "(" 3 digits ")" 3 digits "-" 4 digits ]
一方から他方へ自動的に移動するために、Claude Sonnet 5 CodeにEmacs Lispファイル(pelide-view.el)を書かせました。これは、T2Tに基づいた小さなパイプラインにバッファ(またはリージョン)をシェルアウトします:OhmJSパーサーとそれに続くRWRリライトセマンティクス。パイプラインは注釈付きソースを読み取り、隠蔽された文法を返します。そして、そのelispコードがエディタに表示されます。
パイプラインのパーサー(pelide.ohm)とそのリライトルール(pelide.rwr)の文法は付録にあり、生成されたpelide-view.elも同様です。残りのコードはgithubリポジトリにあります。
利用可能な時間のため、私はこの例を上記の1つに対してのみ実行しました。それがより複雑な文法に一般化するかどうかは未解決の問題です。
再現するには:pelide-view.elを評価してロードし、Emacsにex.rをロードしてから、C-c p tを押して隠蔽ビューに切り替え、もう一度押して戻ります。
観察
パイプラインは期待どおりに機能しました:コピー句と末尾のアクションブロックを隠蔽し、上記のベア文法を残しました。Emacsでの切り替えは期待どおりに機能しました。
内部的には、これは私のT2Tトランスモグリフィア技術が機能しています:OhmJSがパースを行い、私がRWRと呼ぶカスタムDSLがフィルタリング、リライト、隠蔽を行います。結果は読み取り専用表示のためにEmacsに返されます。
結論
この1つの例からでも、隠蔽された文法は注釈付きのオリジナルよりも読みやすく、一致しているパターンが一目でわかりやすくなっています。これは一般的なアイデアにとって良い兆候です。
強い結論を出す前に、より多くの文法で試してみたいほど小さな結果ですが、追求する価値があるようです。自然な次のステップは、他のより現在のPEG実装(LuaやJanetのものなど)の隠蔽文法を書くことでしょう。
これはまた、T2Tをフィルタとして使用することが現代のハードウェアで高速かつ使用可能であることを示唆しています — 切り替え時の遅延はほとんど知覚できません。小さな言語を小さく保つ限り、それは変わらないと予想しています:ここではチューリング完全な巨大言語の文法を書く必要はありません。小さなコードスニペットで十分だからです。
この実験は、フォローアップの問題も示唆しています。それは、ファイル全体が単一の隠蔽可能な単位であると仮定するのではなく、より大きなファイル内で隠蔽可能なコードの存在を検出できる文法を書くことです。
さらなるアイデア:1つのパラダイムではなく、多くの小さな言語を構成する
この実験は、より大きな質問の一部です:すべてのものを1つの汎用言語に押し付けるのではなく、どのようにして多くの小さなDSLが単一のプロジェクト内で共存できるようにするか?
すでに進行中の方向性の1つは、隠蔽ではなくトランスモグリフィケーションです:小さなメタ言語で書き、そこから標準的なターゲット言語を生成することです。PBPカーネルはこのように構築されています — .rtメタ言語で書かれた3つのソースファイル(0d.rt、stock.rt、jit.rt)が、現在Python、JavaScript、Common Lispにトランスモグリフィケーションされています。トランスモグリフィア自体は機能しますが(詳細についてはカーネルリポジトリを参照)、まだプロジェクショナルビューイング実験をそれに実行していません。
さらに先の方向性は、ダイアグラムプログラミングです:テキストではなく、ダイアグラムプログラミング言語(DPL)からコードスニペットを生成することです。これにはdrawioを使用した小さな初期例があります(決定木リポジトリとステートマシンリポジトリ)が、ここでも隠蔽実験はそれらに対して行われておらず、drawioがEmacsとどのように連携するかさえまだ明らかではありません。
付録 — 生成されたpellide-view.elコード
;;; pellide-view.el --- PELLIDEにシェルアウトして簡潔なビューを表示 -*- ;;; lexical-binding: t; -*- (defgroup pellide-view nil "Shell buffer/region out to the PELLIDE tool for a condensed view." :group 'tools) (defcustom pellide-view-command "pellide" "Shell command to run. Reads source on stdin, writes condensed view on stdout." :type 'string) (defcustom pellide-view-command-args nil "Extra CLI args to pass to `pellide-view-command'." :type '(repeat string)) (defvar-local pellide-view--original nil "Buffer-local stash of the pre-toggle source, used by `pellide-view-toggle-in-place'.") (defun pellide-view--run (text) "Pipe TEXT through `pellide-view-command'; return its stdout as a string." (with-temp-buffer (insert text) (let ((exit-code (apply #'call-process-region (point-min) (point-max) pellide-view-command t t nil pellide-view-command-args))) (unless (zerop exit-code) (error "%s exited %s: %s" pellide-view-command exit-code (buffer-string))) (buffer-string)))) ;;;###autoload (defun pellide-view-region-or-buffer () "Shell the region (or whole buffer) out to PELLIDE; show the result read-only in a `*PELLIDE View*' side window. Non-destructive -- source buffer is untouched." (interactive) (let* ((text (if (use-region-p) (buffer-substring-no-properties (region-beginning) (region-end)) (buffer-string))) (mode major-mode) (result (pellide-view--run text)) (out-buf (get-buffer-create "*PELLIDE View*"))) (with-current-buffer out-buf (let ((inhibit-read-only t)) (erase-buffer) (insert result) (funcall mode) (setq buffer-read-only t))) (display-buffer out-buf '((display-buffer-reuse-window display-buffer-in-side-window) (side . right) (window-width . 0.5))))) ;;;###autoload (defun pellide-view-toggle-in-place () "Toggle the CURRENT buffer's text between source and PELLIDE's condensed view. Destructive: only safe if you don't edit while in the condensed view -- PELLIDE is not assumed invertible, so toggling back just restores the stashed original, discarding any edits made to the condensed text." (interactive) (if pellide-view--original (let ((orig pellide-view--original) (modified (buffer-modified-p))) (setq pellide-view--original nil) (setq buffer-read-only nil) (erase-buffer) (insert orig) (set-buffer-modified-p modified)) (let ((orig (buffer-string)) (modified (buffer-modified-p))) (erase-buffer) (insert (pellide-view--run orig