2013年1月29日火曜日

ClojureのKeywordはLensなのですy

春休みヤッター!
ブログの更新頻度も上げていくつもりです。

Lens


Lensはあるフィールドに対するgetterとsetterを持つ。
コードにするとこんな感じ。

(defprotocol Lens
(lget [lens object])
(lset [lens object value]))
(defrecord Rational [numer denom])
(def numer
(reify Lens
(lget [lens object]
(get object :numer))
(lset [lens object value]
(assoc object
:numer value))))
(def denom
(reify Lens
(lget [lens object]
(get object :denom))
(lset [lens object value]
(assoc object
:denom value))))
view raw lens.clj hosted with ❤ by GitHub

user> (lget numer (->Rational 1 2))
1
user> (lset denom (->Rational 1 2) 3)
#lens.Rational{:numer 1, :denom 3}
view raw lget.clj hosted with ❤ by GitHub

見てわかるとおり、lgetはgetで、lsetはassocで十分なのだ。

user> (get (->Rational 1 2) :numer)
1
user> (assoc (->Rational 1 2) :denom 3)
#lens.Rational{:numer 1, :denom 3}
view raw getassoc.clj hosted with ❤ by GitHub

Lensの特徴


getとsetがあればmodifyが定義できる。

(defn modify [lens object f]
(lset lens object (f (lget lens object))))
view raw modify.clj hosted with ❤ by GitHub

user> (modify numer (->Rational 1 2) inc)
#lens.Rational{:numer 2, :denom 2}
view raw modifynumer.clj hosted with ❤ by GitHub

Clojureにはupdate-inがある。

user> (update-in (->Rational 1 2) [:numer] inc)
#lens.Rational{:numer 2, :denom 2}
view raw updatein.clj hosted with ❤ by GitHub

Lensは合成が可能だ。

(defn compose [lens lens']
(reify Lens
(lget [this object]
(lget lens (lget lens' object)))
(lset [this object value]
(lset lens (lget lens' object) value))))
view raw compose.clj hosted with ❤ by GitHub

user> (lget (compose numer denom) (->Rational 1 (->Rational 2 3)))
2

Clojureの{get, assoc, update}-in関数は複数のKeywordをとることで、ネストしたレコードに対しても有効です。

user> (get-in (->Rational 1 (->Rational 2 3)) [:denom :numer])
2
view raw getin.clj hosted with ❤ by GitHub

LensはStateモナドと組み合わせると素敵です。
更新途中の状態を取得することは出来ないけれど、Clojureにはアローマクロが存在します。

(defn add [rational {:keys [numer denom]}]
(-> rational
(update-in [:denom] (partial * denom))
(update-in [:numer] #(+ (* % denom) (* numer (:denom rational))))))
view raw arrow.clj hosted with ❤ by GitHub

user> (add (->Rational 1 2) (->Rational 2 3))
#lens.Rational{:numer 7, :denom 6}
view raw add.clj hosted with ❤ by GitHub

KeywordがLensだということを意識すれば、Lensの考え方を応用することが出来ますね。