recordを使ってデータ構造をつくる
SICPまじ楽しいな。この本は、一読して内容を理解するだけじゃなくて、手を動かしてコードを写経したり練習問題をやると、純粋にプログラミングの楽しさを味わえる。本質的な部分(パラダイム)を濃密に扱っているので、数学と一緒で、手を動かして思考を追う過程に悦びがある。
まだ2章の途中だが、データ構造をLISPの流儀で定義するところが、考え方としてはすごくシンプルでエレガント。 しかし、何度味わっても美味しい。(いや、逆にシンプルゆえに美味しいのかもしれない)
手続き型言語やオブジェクト指向言語みたいにstructやclassを使ってデータ構造そのものを定義するのではなく、関数というインタフェースだけを使って、データの抽象概念を見せる。
さて、練習問題2.2では、線分を作ってその線分の中点を求めるために、点、線分を扱う関数を定義する。 使う側からすると以下のようになる。
(def seg (make-segment (make-point 10 20) (make-point 20 30))) (x (midpoint-segment seg)) ; -> 15 (y (midpoint-segment seg)) ; -> 25
OOPっぽく書けば
seg = Segment.new( Point.new(10,20), Point.new(20, 30)) seg.midpoint-segment.x # -> 15 seg.midpoint-segment.y # -> 25
(x-point (midpoint-segment seg))
とseg.midpoint-segment.x-point
は順番が逆になっただけで、記述量に違いはない。
しかし、考え方が真逆なのが、やはり面白い。
関数の定義は、SICP流に従ってClojureで書くと下記のとおりになる。
(defn make-point [x y] (list x y)) (defn x [p] (first p)) (defn y [p] (last p)) (defn make-segment [start end] [start end]) (defn start [s] (first s)) (defn end [s] (last s)) (defn midpoint-segment [s] (let [ss (start s) es (end s)] (make-point (/ (+ (x ss) (x es)) 2) (/ (+ (y ss) (y es)) 2))))
これをClojureのrecordと使うと、ぐっと簡単に書ける。
midpoint-segment
はデストラクテャリングを使って記述を短くしている。
(defrecord Point [x y]) (defrecord Segment [start end]) (defn midpoint-segment [{:keys [start end]}] (->Point (/ (+ (:x start) (:x end)) 2) (/ (+ (:y start) (:y end)) 2)))
recordは内部的にはJavaのclassそのもので、データ構造を定義できて、なおかつClojureで定義したものをJavaの世界からでも参照できる。 まさにJavaとのinteroperabilityのための機能だ。
recordのいいところはmapと同等の機能を持つ点で、要素を取り出すための関数定義x
、y
、start
、end
が不要で、
defrecord時の引数に渡す要素名がそのままkeyとして使える。
さらに、生成関数を自前で定義する必要ないという点もある。
(->Segment)
と書けばSegment.new()
に相当する。(Segment.)
と書いても動作する。
このJavaからのいいとこ取り、しかもwell-designedなところが、Clojureのセクシーなところだ。
最初の使用例を書き直すと下記になる。
(def seg (->Segment (->Point 10 20) (->Point 20 30))) (:x (midpoint-segment seg)) ;-> 15 (:y (midpoint-segment seg)) ;-> 25
最後の2行は、スレッディングマクロを使えば、さらにOOPっぽくかける。
(-> seg midpoint-segment :x) ;-> 15 (-> seg midpoint-segment :y) ;-> 25
以上。