📝マルチメソッド(multimethod)とは

Clojure独特の機能. ひと言でいうと, 条件分岐がしたくなったら美しくキメる技.

Clojure: switch文(cond/case)の進化系. 操作の条件分岐の複雑さの課題(Expression Problem)を解決するシンタックス.


Javaの オーバーライドをClojureで実現する方法.

同一名称のメソッドで異なる型によって処理を分けるようなことをしようとするとClojureでは,

  • defmulti で複数の処理を分岐させるための同一名称のメソッドを定義.
  • defmethod で異なる処理を型ごとに記述.

defmultiで与えられた引数のどれをdispatchのための分岐条件にするかを定義する. いいかえれば, 引数をばらして分岐条件を決める. defmethodで分岐条件に基づいて与えられた引数を処理する.

JavaだとClassによって異なる実装を呼び出す, Clojureだと引数すべてにおいてディスパッチ可能という点においてJavaより強力な文法になっている.

quick example

引数 sに対するバラし方を定義.

(defmulti my-print class)

class は (fn [x] (class x)) という無名関数の糖衣構文にすぎない.

引数sがstring, つまり (class s) => String ならば(.write out s)を実行.

(defmethod my-print String [s] (.write *out* s))
(defmethod my-print nil [s] (.write *out* "nil"))

multimethod呼び出し.

(my-print "stu")

操作対象のグループ化(derive/isa?)

protocolが操作のグループ化に対してmultimethodは操作対象のグループ化ができる. ディスパッチ値とそれに対する derive/isa? を利用する. deriveとは派生(derivation)の動詞の意. deriveで関係を定義してisa?で関係をテストする.

Clojureは継承をサポートしていないので型の親子関係を表現できないが, dervieによりディスパッチする対象に対する親子関係を定義することができる.

ref. derive - clojure.core | ClojureDocs

ディスパッチ値

名前空間で修飾されたシンボルとキーワードで表現される特殊形式. (:名前空間)/(keyword)のように表現されるが, 実際の仕様例では(::keyword)と省略表現されていることが多い. ダブルコロン(::)は 名前空間を指すリーダマクロ.

::rect
;;-> :user/rect

🔧Clojure Integrant でもよくみかける表記でありここからディスパッチ値の存在を知る人も多いと思う.

親子関係の定義(derive)

(derive 子供 親) を使って階層的な関連を定義.

(derive ::rect ::shape)
(derive ::square ::rect)

もしくはディスパッチ値の代わりにJavaクラスも定義可能.

(derive java.util.Map ::collection)
(derive java.util.Collection ::collection)
 
(isa? java.util.HashMap ::collection)
-> true

✅Clojure multimethod Howto

特定条件によるif文, switch文, case文のおばけになりそうなときは積極的に導入したいところ. 型とはクラスやkeywordではなく分岐したい全て, ということでいろいろまとめる.

キーワードで処理を分岐したい

キーワードに応じてデータを処理する例.

(defmulti ->foo (fn [x y] x))
(defmethod ->foo :type-a [x y] y)
(->foo :type-a {})

キーにbindされているvalueで処理を分岐したい

(def m {:a "hoge"})
 
(defmulti ->foo :a)
(defmethod ->foo "hoge" xxx)

defmultiのパラメータでキーワードが与えられても実際は (fn [x] (get x :a)) の糖衣の関数であるところはハマった…

複数パラメータは でいける(ref. juxt).

;; Define the multimethod
(defmulti service-charge (juxt :id :tag))
 
;; Handlers for resulting dispatch values
(defmethod service-charge [::acc/Basic ::acc/Checking] [_] 25)
(defmethod service-charge [::acc/Basic ::acc/Savings]  [_] 10)

defmethodを異なるnamespace(file)で定義したい

いわゆる Open-Closed Principle をClojureでどうやるかというはなし.


単一の ifにパラメータで分岐条件を渡してそれぞれのドメインのメソッドを呼び出したい. case文で条件分岐をしようとすると, サブドメインのnamespaceをrequireする必要がある.

これだけならいいのだが,それぞれのサブドメインから共通処理をヘルパー関数として呼び出したいとき, cyclic dependenciesが発生してしまう.

この場合interface用のnamespaceを作成してそこにdefmultiを定義する. 詳しは以下のstackoverflow参照

ref. protocols - Using Clojure multimethods defined across multiple namespaces - Stack Overflow


ただこの場合はmultimethodではなくprotocolを利用するべきかもしれない. namespaceというのが複数のデータと操作をひとつの環境にbindするのならば複数の操作が前提となり, それは操作のグルーピングを担う protocolの役目.

see also. 💡Clojure multimehod vs protocols 比較

デフォルトメソッドを設定したい

defmethodの2つめの引数に :default を設定する.

Clojure multimethod Topics

💡いつマルチメソッドを使うべきか?

ref.) 📚Programming Clojure より引用.

筆者がどんとなころでマルチメソッドが使われているか調査したときの発見.

  • マルチメソッドは めったに使われない.
  • マルチメソッドはクラスによりDispatchしていることが多い.

筆者による提案.

  • 関数が1つまたは複数の型によって分岐していたら, マルチメソッドを検討する. 型とはクラスやkeywordに限らない. あなたが分岐するものと感じるもの.
  • 判断に迷ったら関数版とマルチメソッド版を両方書いてみて読みやすい方を選ぶ.

🤔もし条件分岐が数個で分岐のパラメータを求めるフォームも単純ならmultimethodはいらないかもしれない. 3つ以上など自分で基準を持っておくのもいいかも(推測だとリフレクションをつかうのかな?そうならば多用するのは問題な気がした).

💡multimehodはcondやprotocolよりも遅いのか?

muitlmethodはおそいのか?だからcondやprotocolがいい?

まあcondやprotocolsと比べると遅い事実はあるものの気にするレベルかはその時のの状況による. たいていはperformanceよりも解決するべき問題を大事にするべきだ, と Alex Miller はいっている.

ref. Performance of multimethod vs cond in Clojure - Stack Overflow

🆚パターンマッチ vs multimethods

Clojureにパターンマッチではなくマルチメソッドがあるのは好みの問題.

🔦Rich Hickeyはパターンマッチよりもポリモーフィズム(multimethods)が好き

Clojure multimethod Insights

🔗References

up: 📁Clojure Expression Problem