LispとGUIの相性 (Scratchpad)

This page is a personal scratchpad.
ここ最近、Glisp の言語処理系の実装を色々と見直しています(class-based-ast)。しかし、Lisp をそれ自体プログラミング言語のためのシンタックスとして維持しつつどのように GUI に対応付けするかという試行錯誤の中で、まだまだ知っておかなきゃいけない概念がたくさんありそうに思えたので、一旦頭の整理に今突っかかっている所をメモしておきます。
Glisp では、Lisp コードを文字列として編集できるほかに、GUI 上からその値を GUI から編集できる機能を提供しています。プリミティブな値が単独で選択された場合はそのリテラルに従って、文字列ならテキストインプット、数値なら数値ボックス、真偽値ならチェックボックスなどと、それぞれに従った GUI を自動で表示します。それが関数の引数として使用された場合、その関数オブジェクトのメタデータに従ってさらに適切な GUI を表示します。例えば、2 つの数値を 0-1 で補間する関数 lerp はこんな感じ。

この第 3 引数は、メタデータ上ではこのようになっています。
{type: "number", ui: "slider", min: 0, max: 1}
同様に文字列の場合も、それが text 関数のように任意の文字列として使われる場合もありますが、stroke 関数の線端の形状のように、列挙型として使われるケースもあります。

Cap に対応する引数のメタデータは以下の通りです。
{type: "string", ui: "dropdown", default: "round",
values: ["butt", "round", "square"]}
同様に、stroke 関数の第一引数である線色もリテラルとしてはただの文字列ですが、セマンティクス的には色を表しています。
{type: "string", ui: "color"}
Lisp コードのそれぞれの値に適切な GUI を表示させようとしたとき、そのリテラルだけでは情報として不十分なことになります。
Glisp ではある値が選択された時(実は現行の版ではリストやマップ以外のプリミティブな値を単独で選択することはまだできませんが)、その値がどの関数の引数として記述されているかを検知した上で、関数のメタデータに従ってさらに適切な UI を表示しようとします。しかし「どの関数」かを知るには、その式を評価する他にありません。つまり、ある式の GUI を適切に表示するには、それ以前にその式が一度は評価されている必要があるのです。これが結構悩ましいところでして。
例えば、式の途中で例外が投げられた時、それ以降の式は評価されないので、
(throw "Error")
(+ 1 2) ;; 1 + 2
(+ 1 2) を選択しても、インスペクタには適切な GUI を表示することができません。
同様に、if のような特殊式やマクロでも、式自体が一度も評価されないことがあります。
(if false (+ 1 2) nil) ;; false ? 1 + 2 : null
上記の場合、評価器はthen にあたる (+ 1 2) をすっ飛ばして、else にあたる nil のみを評価するので、(+ 1 2) に対応する GUI を表示することができません。
ここで式自体の評価とは別に、関数の名前解決を先に済ませてしまえば? という発想にもなります。つまり、関数シンボル名とその式が実行されるスコープに基づいて関数オブジェクトを前もって知っておけばいいのです。しかし以下のような場合にはこの作戦は破綻します:
;; A. 即時関数
((fn [a b] (+ 1 2)) 1 2)
;; B. 関数部分に式を含む
((if true + -) 1 2)
;; C. 同一スコープの中で宣言されたシンボルを用いて関数呼び出しをする
(def - +)
(- 1 2)
A、B は、単にリストの 1 番目の式のみを評価してしまえば済むことですが、C の場合は、結局 (def - +) の式全体を評価してあげないからには、その外側のスコープで宣言されているであろう間違った関数オブジェクト(引き算)を参照してしまうことになります。
def や let のようなスコープに関わる特殊式に関しては予め式全体を評価することにしたとしても、その中で例外が投げられれば同じことですし、def 自体がマクロの内側で呼び出されていたら判別しようがありません。
もう一つ思いついた対策は、その式が評価される際に与えられるスコープにおいて名前解決されるよう強制する構文を用意することです。具体的に言うと、Glisp(のベースになっている Make-A-Lisp プロジェクト)の Lisp 評価器には、その式とスコープがセットで与えられます。ちょうど eval("(+ 1 2)", scope) のような形で。scope は、シンボル名と値からなるマップオブジェクトのようなものです。例えばですが、$ を特殊文字として $(+ 1 2) のような形で関数呼び出しをすると、この + は必ず scope において静的に名前解決されるというルールにしてしまえば、式全体を評価せずとも関数を知ることができます。
(let [+ -]
$(+ 1 2))
の場合は、 + は let スコープではなく、評価器に与えられたスコープにおいて名前解決されます。
とかとか、ややこいことを考えてしまっています…。別方面の対策としては GUI に必要な情報を関数のメタデータによって補間するのではなく、その値自身のメタデータとして付与するのもアリかもしれません。その場合コードが冗長になって嫌なんですが、GUI 側から一度 tweak すると、コードの引数部分に勝手にメタデータがくっついて、二度目以降は評価無しに正しく GUI が表示されるっていう仕組みとか。
(lerp ^{:type "number"} 0
^{:type "number"} 0
^{:type "number" :ui "slider" :min 0 :max 1} .5)
Common Lisp のように、関数オブジェクトの名前空間を分けてあげるともしかしたら何か解決するかもしれないです。あるいは第一級オブジェクト的な構文を諦めるとか…。
関数適用は、例えばノードベース UI でいう所のノードそのものになりますが、そのノードの種類は GUI に表示された時点で既に決定されています。((if true + -) 1 2) をノード UI に置き換えるならば、そのノード全体が評価(Cook)される時になって動的にノードの種類が定まるという奇妙なことになってしまいます。「同一の引数を、条件によって異なる関数に与えてあげる」という処理は、多くのビジュアルプログラミング言語では (if true (+ 1 2) (- 1 2)) のような若干冗長な方法を取らざるを得ないはずです。(ノードベースなら、1, 2にあたるノードから + - にあたるノードの入力に「分流」させる方法を取ることが出来るので、若干マシですが)
「Lisp ベースのビジュアルプログラミング言語としてのデザインツール」という大層なコンセプトを掲げてしまっているのですが、突き詰めるほど、GUI ベースのビジュアルプログラミングが文字列ベースのコーディングにその柔軟性や素早さで敵わないということが身に沁みます。
