TL;DR

  • Clojure のオブジェクトは装飾された単なるマップで、defrecord で定義したレコードから ->Car や map->Car で作る。
  • メソッドを持たせるには、defprotocol でプロトコルを定義し、レコードに継承させる。extend-protocol を使うと、既存のレコードにも継承させられる。

内容

Clojure は関数型プログラミングの言語だが、OOP のようにオブジェクトを作ったり操作したりできる。ただし、Clojure のオブジェクトは装飾された単なるマップで、それ以上でもそれ以下でもない。必ずしも使う必要はないが、使うことはできる。

レコードの定義とオブジェクトの生成

Clojure におけるクラスは、レコード(record)と呼ばれる。レコードは以下のように defrecord で定義する。

(defrecord Car [name top-speed])

オブジェクトを作るには、->レコード名 関数を呼び出す。このとき渡す値はレコードを定義したときと同じ順番にする必要がある。

(def tata-nano (->Car "Tata Nano" 120))

作ったオブジェクトは装飾された単なるマップなので、マップと同じようにフィールドにアクセスできる。

(:name tata-nano)      ; "Tata Nono"
(:top-speed tata-nano) ; 120
 
(get tata-nano :top-speed) ; 120

map->レコード名 関数を使うと、マップを元にオブジェクトを作れる。

(def ambassador (map->Car {:name "Ambassador"
                           :top-speed 240}))

こちらも、マップのようにフィールドにアクセスできる。

(:name ambassador)          ; "Ambassador"
 
(get ambassador :top-speed) ; 240

オブジェクトを関数の引数に渡す

以下のように、Car のオブジェクトを受け取ることを想定した関数を定義する。

(defn describe [vehicle]
  (let [{name :name top-speed :top-speed} vehicle]
    (str name " has a top speed of " top-speed " Km/hr.")))

このコードでは let の中で引数 vehicle の :name を変数 name に、:top-speed を変数 top-speed に束縛している。

先ほど定義した Car のインスタンスを作ってこの関数に渡すと、想定どおりに動く。

(describe tata-nano)
;; "Tata Nano has a top speed of 120 Km/hr."

次に、同じフィールド(:name と :top-speed)を持つ別のレコードを定義する。

(defrecord Ship [name top-speed])

同じように、Ship のオブジェクトを受け取ることを想定した関数を定義し、動作を確かめる。

(def arctic-explorer (->Ship "Arctic Explorer" 10))
 
(defn describe-ship [vehicle]
  (let [{name :name top-speed :top-speed} vehicle]
    (str name " has a top speed of " top-speed " knots.")))
 
(describe-ship arctic-explorer)
;; "Arctic Explorer has a top speed of 10 knots."

プロトコル

Clojure で OOP のメソッドのようなものを使うには、はじめにプロトコル(インタフェース)を定義する必要がある。そして、レコードを定義するときにプロトコルを継承して、はじめてメソッドを定義できる。

ここでは例として、「オブジェクトを関数の引数に渡す」で作った関数をプロトコルに置き換える。プロトコルは defprotocol で以下のように定義する。

(defprotocol Describe
  (describe [this]))

そして、Car を定義するときにプロトコルを継承することを宣言し、メソッドを定義する。メソッドを定義するときは defn は要らない。

(defrecord Car [name top-speed]
  Describe
  (describe [this]
    (let [{name :name top-speed :top-speed} this]
      (str name " has a top speed of " top-speed " Km/hr."))))

この this は Clojure の特別な変数ではなく、単なる変数である。

同じように、Ship も定義する。

(defrecord Ship [name top-speed]
  Describe
  (describe [this]
    (let [{name :name top-speed :top-speed} this]
      (str name " has a top speed of " top-speed " knots."))))

メソッドは通常の関数と同じように呼び出す。

(def tata-nano (->Car "Tata Nano" 120))
(def arctic-explorer (->Ship "Arctic Explorer" 10))
 
(describe tata-nano)
;; "Tata Nano has a top speed of 120 Km/hr."
 
(describe arctic-explorer)
;; "Arctic Explorer has a top speed of 10 knots."

既存のレコードにプロトコルを継承させる

extend-protocol を使うと、すでにあるレコードにプロトコルを継承させられる。

以下のような Rocket というレコードがすでに定義されているとする。

(defrecord Rocket [name orbit])

この Rocket の定義を変えずに Describe プロトコルを継承させるには、以下のように書く。

(extend-protocol Describe
  Rocket
  (describe [this] (str (:name this) " reaches " (:orbit this) " orbit.")))

実際にオブジェクトを作ってメソッドを呼び出すと、想定した結果が出力される。

(def pslv (->Rocket "PSLV" "Low Earth"))
(describe pslv)
;; "PSLV reaches Low Earth orbit."

キーワード