::rect
-> :user/rect
Clojure は状況ごとに新しいデータ型を定義するような伝統的オブジェクト指向のアプローチを避け、代わりに少ないデータ型に対する関数群からなる巨大なライブラリを構築することを好む。しかしながら、Clojureも柔軟で拡張可能なシステムアーキテクチャを可能にするランタイムポリモーフィズムの価値は認識している。Clojureはタイプ、値、属性と引数のメタデータ、ひとつ以上の引数の関連性によるディスパッチをサポートするマルチメソッドシステムを通して洗練されたランタイムポリモーフィズムを提供する。
Clojureのマルチメソッドは ディスパッチ関数 と一つ以上の メソッド で定義される。 defmulti を使用してマルチメソッドを定義する場合、ディスパッチ関数を与える必要がある。この関数が ディスパッチ値 の生成のためにマルチメソッドの引数に適用される。マルチメソッドはその後ディスパッチ値、もしくはディスパッチ値から派生した値と関連するメソッドを探索しする。関連するメソッドが定義されている場合、( defmethod 経由)、そのメソッドディスパッチ値を引数として呼び出される。ディスパッチ値と関連するメソッドの定義が存在しない場合、マルチメソッドはデフォルトのディスパッチ値と関連するメソッドを探索し(デフォルトは :default)、存在する場合それを仕様する。存在しない場合、マルチメソッドの呼び出しはエラーとなる。
マルチメソッドシステムはこのAPIを提供している: defmulti マルチメソッドを新しく作成する、 defmethod ディスパッチ値と関連するメソッドをマルチメソッドに新しく定義する。 remove-method ディスパッチ値と関連するメソッドを削除する。 prefer-method メソッドの間の優先順位が曖昧な場合に優先順位を定義する。
派生(derivation)はJavaの継承(クラス値)、もしくはClojureのアドホックな階層システムの組み合わせで判断される。階層システムは名前(シンボル、もしくはキーワード)間、またはクラスと名前の関連性を利用した派生をサポートしている。 derive 関数はこの関連性を作成し、 the isa? 関数が関連性の存在をテストする。 isa? と instance? は異なる点に注意。
(derive child parent) を使って階層的な関連を定義することができる。子と親には名前空間で修飾されたシンボルとキーワードを使用することができる:
注: :: のリーダー文法をキーワードに使用すると(::keywords)名前空間に解決される。
::rect
-> :user/rect
基本的には deriveを使って関係性を作る。
(derive ::rect ::shape)
(derive ::square ::rect)
parents / ancestors / descendants と isa?を使うことで階層について調べることができる。
(parents ::rect)
-> #{:user/shape}
(ancestors ::square)
-> #{:user/rect :user/shape}
(descendants ::shape)
-> #{:user/rect :user/square}
(= x y)
が真なら (isa? x y)
も真となる。
(isa? 42 42)
-> true
`isa?`は階層システムを使う。
(isa? ::square ::shape)
-> true
クラスを子として使うこともできる。(親としては使えない、何かのクラスの子を定義することはJavaの継承関係を通してしかできない)。
これにより既存のJavaクラスの階層に新たな分類を重ねて定義することが可能になる:
(derive java.util.Map ::collection)
(derive java.util.Collection ::collection)
(isa? java.util.HashMap ::collection)
-> true
isa? はクラス間の関連性についてもテストする。
(isa? String Object)
-> true
parents / ancestors も同様。(descendants は子孫はオープンセットであるため異なる。)
(ancestors java.util.ArrayList)
-> #{java.lang.Cloneable java.lang.Object java.util.List
java.util.Collection java.io.Serializable
java.util.AbstractCollection
java.util.RandomAccess java.util.AbstractList}
(isa? [::square ::rect] [::shape ::shape])
-> true
(defmulti foo class)
(defmethod foo ::collection [c] :a-collection)
(defmethod foo String [s] :a-string)
(foo [])
:a-collection
(foo (java.util.HashMap.))
:a-collection
(foo "bar")
:a-string
prefer-method はディスパッチ値が複数マッチし、どちらを優先するべきか曖昧な場合に順序を付けることに使用する。マルチメソッド毎に特定のディスパッチ値が別のディスパッチ値よりも優先されることを宣言することができる:
(derive ::rect ::shape)
(defmulti bar (fn [x y] [x y]))
(defmethod bar [::rect ::shape] [x y] :rect-shape)
(defmethod bar [::shape ::rect] [x y] :shape-rect)
(bar ::rect ::rect)
-> Execution error (IllegalArgumentException) at user/eval152 (REPL:1).
Multiple methods in multimethod 'bar' match dispatch value:
[:user/rect :user/rect] -> [:user/shape :user/rect]
and [:user/rect :user/shape], and neither is preferred
(prefer-method bar [::rect ::shape] [::shape ::rect])
(bar ::rect ::rect)
-> :rect-shape
上記の全ての例ではマルチメソッドシステムが使用するグローバルな階層を使用しているが、 make-hierarchy で完全に独立した階層を作ることも可能で、上記の全ての関数は任意で階層を第1引数にとることができる。
このシンプルなシステムは非常に強力だ。Clojureのマルチメソッドと伝統的なJavaスタイルのシングルディスパッチの関係を理解するひとつの方法は、シングルディスパッチはClojureのマルチメソッドにおいてディスパッチ関数が第一引数に対してgetClassを呼び出し、かつメソッドがそのクラスと関連しているようなものだ。Clojureのマルチメソッドはクラスや型と密に結びついていないため、複数の引数のどのような属性に基づくことができ、また引数に対するバリデーションを行いエラー処理メソッドを呼び出すこと等が可能になる。
注: この例ではキーワード :Shape がディスパッチ関数として使われている。 データ構造 のセクションで説明されている通り、キーワードはマップに対する関数だからだ。
(defmulti area :Shape)
(defn rect [wd ht] {:Shape :Rect :wd wd :ht ht})
(defn circle [radius] {:Shape :Circle :radius radius})
(defmethod area :Rect [r]
(* (:wd r) (:ht r)))
(defmethod area :Circle [c]
(* (. Math PI) (* (:radius c) (:radius c))))
(defmethod area :default [x] :oops)
(def r (rect 4 13))
(def c (circle 12))
(area r)
-> 52
(area c)
-> 452.3893421169302
(area {})
-> :oops