Programmer's Note

コード読み書きの備忘録。

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と同等の機能を持つ点で、要素を取り出すための関数定義xystartendが不要で、 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

以上。