橋本 麦∿Baku Hashimoto

LispとGUIの相性

ここ最近、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 - +) の式全体を評価してあげないからには、その外側のスコープで宣言されているであろう間違った関数オブジェクト(引き算)を参照してしまうことになります。

deflet のようなスコープに関わる特殊式に関しては予め式全体を評価することにしたとしても、その中で例外が投げられれば同じことですし、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ベースのビジュアルプログラミングが文字列ベースのコーディングにその柔軟性や素早さで敵わないということが身に沁みます。