Clojureにおけるポリモーフィズム解説
リスプであるClojureではJavaのような型でのOOPとは一味違った形でポリモーフィズムを実現できます。
Clojureではポリモーフィズムのサポートとして
- defmulti
- protocol
というマクロが用意されています。
defmulti
使い方
(defmulti name docstring? attr-map? dispatch-fn & options)
簡単な例として、いろいろな図形の面積を求める公式をdefmultiを使って書いてみたいと思います。
世の中には三角形や円など、様々な図形があり、どの図形も「面積」を持っています。
面積を求める公式は図形ごとに異なっていますが、全て「面積」ということは共通しているので、一貫した扱いをしたいところ。
ここでは図形の面積を求める関数をareaとして、defmultiを使って実装してみます。
まず、「長方形」と「円」のコンストラクターを用意します。
(defn rectangle [w h] {:width w :height h :id ::rect})
(defn circle [r] {:radius r :id ::circle})
次に、これらの面積を計算する関数areaをdefmultiで宣言しましょう。
(defmulti area (fn [x] (:id x)))
ここで、ディスパッチとして図形の:idを使用していることに注意してください。
例えば、xが上記のrectangleの結果として出た値ならば、(:id x)は::rectとなるので、後ほど定義する::rectに対応したareaのバージョンがそのxに対して適応されることになります。
それでは、実際に、::rectと::circleに対応したareaを実装していきます。
(defmethod area ::rect [x] (* (:width x) (:height x)))
(defmethod area ::circle [x] (* (:radius x) (:radius x) Math/PI))
これで、それぞれの図形にふさわしく面積を求める関数ができました。
それでは、実際に長方形と円の面積をareaで計算してみます。
(def r (rectangle 10 5))
(def c (circle 3))
(area r) ;; => 50
(area c) ;; => 28.27
以下のように、areaに渡せる図形のコレクションから、それらの面積の和を求めることも容易になります。
(defn sum-area [coll] (reduce + (map area coll)))
(sum-area [(rectangle 10 5) (circle 3)]) ;;=> 78.27 (= 50 + 28.27)
プロトコル
defprotocolは他のオブジェクト指向言語でのインターフェースに対応する機能を提供します。つまり、関数の「型」だけ定義し、実装は後回しにします。
上で行ったうような異なる図形の面積計算なども、プロトコルを使って行えます。
そのためには、まず、「面積」が計算できる図形を表すプロトコルを宣言します。
(defprotocol Shape
(area [this]))
ここでは長方形や円などの具体的な図形をレコードを使って表現してみます。
(defrecord Rectangle [width height]
Shape
(area [this] (* width heigt)))
(defrecord Circle [radius]
Shape
(area [this] (* radius radius Math/PI)))
レコードのインスタンスを生成するには、下記のように
(クラス名)+ドット
と書くか、newというマクロを使います。
(def r (Rectangle. 5 3)) ;; or (new Rectangle 5 3)
(def c (Circle. 3)) ;; or (new Circle 3)
レコード型はマップのように、キーワードでフィールドを取り出したり、updateなどマップに適用できる関数が使えます。
(:radius c) ;=> 3
(update c :radius inc) ;=> Circle{:radius 4}
プロトコルの関数として実装されているareaの値もチェックしましょう。
(area r) ;; => 15
(area c) ;; => 28.27
## プロトコルとdefmultiの組み合わせ
プロトコルとdefmultiを組み合わせれば、さらに柔軟なポリモーフィズムが実現できます。
ここでは、例として上で定義した長方形と円に対して相似な拡大を行う
scale
という関数を作ってみます。
この場合、分岐の基準として
class
が役に立つでしょう。この関数は
(class x)
でxのクラスを返します。
例:
(class r) ;;=> Rectangle
それでは、classをディスパッチ関数としてマルチメソッドを作ってみましょう。
(defmulti scale (fn [x s] (class x)))
(defmethod scale Rectangle [x s]
(-> x (update :width (partial * s)) (update :height (partial * s))))
(defmethod scale Circle [x s]
(update x :radius (partial * s)))
結果:
(scale r 2) ;;=> Rectangle{:width 10, :height 6}
(scale c 2) ;; => Circle{:radius 6}
無事に相似拡大されていました。
ついでに「面積比は相似比の二乗に比例する」という図形の性質も確かめてみましょう。(一つの事例だけでは「証明」にはなっていませんが、ここではとりあえず関数を使ってみるのが目的です。)
(= (* 2 2 (area c) ) (area (scale c 2))) ;; => true
想定した結果になりました。
まとめ
今日はClojureでポリモーフィズムを担うプロトコルとマルティメソッドについてご紹介しました。この2つの機能を使いこなせば、非常に柔軟にポリモーフィズムを実現できるでしょう。
今回の内容は以上になります。最後までお読みいただきありがとうございました。