レガシーコード生産ガイド

私に教えられることなら

近況とLateBinding

DenLispでVirtualDOMライブラリ等ができたので、日夜破竹の勢いでアプリケーションを書いている。

できるだけLateBindingを意識したので、一部の変更で全体を再コンパイルしたりする必要はない。例えばある関数だけをコンパイルすればそれを呼び出す関数の動作も変わる。

しかしやっぱり、設計によっては、モジュール全体の再コンパイルやVDOM全体の再描画を明示的に行う必要になることも多々ある。

考えを記録しておきたいのでDenLispでの例を載せる。世の中に自分しかユーザがいない言語だけど、まあClojureとほとんど同じなのでそういうアレで……

例1 値の参照

なんかhtml的なやつを配列で返すと上手い具合に表示してくれるとする。

;; FooViewモジュール内
(def accent-color "#cca")

(def view [:div {:color accent-color} "hello"])

(def component view)

;; 上記を使う別のモジュール内
(defn render []
  (vdom/render FooView/component))

で、accent-colorの値を変更して定義しなおしたとする。

しかしFooView/viewの値は変わらない。何故ならFooView/viewの定義時にaccent-colorは参照されて"#cca"を返している。

変更を反映する方法として、関数として定義して、毎回呼び出してやる方法がある。

で、次のように変更する。

;; FooViewモジュール内
(def accent-color "#cca")

(defn view []
  [:div {:color accent-color} "hello"])

(def component (view))

;; 上記を使う別のモジュール内
(defn render []
  (vdom/render FooView/component))

まだ問題がある。今度はFooView/componentの定義時にFooView/viewは呼び出されているので、accent-colorの変更は反映されない。

よってこれも同様に関数とし、さらに使用する場所でもそれを考慮してコードを書き換える。

;; FooViewモジュール内
(def accent-color "#cca")

(defn view []
  [:div {:color accent-color} "hello"])

(defn component []
  (view))

;; 上記を使う別のモジュール内
(defn render []
  (vdom/render (FooView/component)))

結構めんどうくさい。書き換えるのやデバッグが面倒なんじゃなくて、関数に関数を重ねて……と包んでいくのが面倒。自動でそういうカンジになるパラダイムが欲しい。

例2 クロージャ

ClojureではなくClosure。生成した関数を使う場合で、こっちはもっと深刻。

以下のようなコードがあったとして

(defn adder [n]
  (fn [x]
    (+ x n)))

(defn makeIncrementer []
  (adder 1))

他の場所でmakeIncrementerで生成した関数を使っているとする。そのオブジェクトが生きているときに、例えば以下のように変更しても

(defn adder [n]
  (fn [x]
    (js/console.log "hello!")
    (+ x n)))

既に生成された関数の動作は変わらない。つまり、makeIncrementeradderを使っている場所を探し出し、その使われ方によっては再コンパイルやオブジェクトを生成する必要がある。

解決策

Smalltalkを使ってみようと思った。メッセージ送信しかなく、生成したオブジェクトの動作を後から変えられるので強そうだと思った。

さらに、前々から考えていた文法も試してみた。Smalltalkとは言いがたい文法かもしれない。例えば次のようなDenLispのコード(これもLispとは言いがたいかもしれない)は

(defn interleave [xs ys]
  let acc []
  let len (length xs)
  (loop [i 0]
    if (>= i len) acc
    (acc.push (at xs i))
    (acc.push (at ys i))
    (recur (inc i))))

次のように記述される。

interleave: xs
  let acc = [].
  let len = self length.

  loop [i = 0] {
    if (i >= len) acc.
    acc push: (self at: i).
    acc push: (xs at: i).
    recur [i inc]
  }

で、DenLispでコンパイラを書いた。中間表現をS式にして、パーサコンビネータJavaScript出力の一部はDenLispのものを使いまわしたので楽にできた。

ライブラリもある程度書いたところで、次のようなことがわかったのでSmalltalkにはしないことにした。完全に個人の感想です。

  • 設計によってはほとんど同じような問題が起きる。LateBinding度がせいぜい60%から70%に上がる程度の印象。
  • クラスオブジェクトとそのクラス等によるネットワークは、DenLispのモジュールに比べると再生成などの見通しが悪い。安心できない。
  • クラスの継承ツリーと、インスタンス←→クラスの関係、2つのグラフがcomplectされてるのは何かが絶対におかしい。何なのかわからん。
  • データが循環参照を持つのは、イベント通知などの単純な双方向リンクを除いて、概念がcomplectされている臭いを感じる。更にそれを基礎の部分に持つのは何かが絶対におかしい。何なのかはわからん。
  • 何にでも解釈されうるデータを型で包むのは概念がcomplectされている臭いを感じる。型はデータではなく動作に存在した方が好ましい。
  • S式というかEDNはとてもいい、楽。

というわけで選択肢を一つ選ばない決断ができた。ついでに(Den)Lispがますます好きになり、いくつか最適化の実験もできた。良かった良かった。

感想

最初は、やはりSmalltalkでやってみるべきか、メタクラスなどの階層はどこから実装するか、ランタイムはどうやって出力するか……などと「設計」しようとして、精神がガンガン削られていった。

アッこれはマズイぞ!と思い、まず一番チープな実装を書いて、それを一番簡単なやりかたで修正して、と繰り返した。結果、精神的には充実した状態のまま、3日ほど夜な夜な作業するだけでできた。

最近邦訳が出たアンチフラジャイルに完全にかぶれてるのもあり、プログラムの設計とか、バグを出さないための云々とか、プログラミングの勉強だとか、そういったものをかなり胡散臭く思っている。ご飯を食べたらうんちが出たから、おしりからうんちを入れるとご飯が出てくる、みたいな話だと思う。汚くてすいません。人間はimmutableでもないしコンテナ型仮想化ができるわけでもないんですよ。試行錯誤によって脳に起きる事をもっと大事にしよう。

広告を非表示にする