Clojureのマルチメソッドについてのメモ

ついさっきPaul Grahamの『ハッカーと画家』を読み終えた。

もともとLisperだということもあってPaul Grahamのサイトでエッサイを読んでいたのだけれど、ついこの前ブックオフに入ったらこの本が中古で売っていたので買ってみた次第。

感想として、Lispが最高と言う結論ではなく(もちろんLispは最高なんだけれど)、それよりもハッカーとして社会で良い仕事をするにはどうすればいいのかということについては今後の人生で長く付き合っていく問題なんだなぁというところ。

将来のことを考えているとプロマネみたいなこともしなければいけないわけで、でも個人的にはハッキング大好きの技術野郎でありたいという気持ちもあって、そこらへんをどう両立していくか、それと同時に良い仕事環境と文化をどう構築するか、などは技術に生きる人間として頭の片隅で意識しておく事柄なのかな、と。

さてさて、気難しい話はここらへんにしておいて今回もClojureの勉強メモを書いていく。

自分は今までCommon Lispの中心にやってきたわけだけど、やっぱりCommon LispClojureは同じLisp言語族でありながらそれなりに違う部分もあるわけで。

そういうことで前にはClojureのリーダマクロについてまとめた。

komi.hatenadiary.com

このエントリーを公開してからClojureハッカーの方々から色々教えてもらい、シンボルと値と名前空間の関係性についての話やメタデータコンパイル速度の話など、とても勉強になった。

ここらへんについてもそのうちまとめておく予定。

で、このエントリーではClojureにおけるマルチメソッドについてまとめようと思う。

マルチメソッドis何

マルチメソッドとは何かというと、簡単にいえば一つの関数名に対して複数の関数を仕込むこと。

多分言葉で説明するより例を出した方が早いと思うので、例えば

(defmulti greeting
  (fn [x] (get x "language")))

;params is not used, so we could have used [_]
(defmethod greeting "English" [params]
 "Hello!")

(defmethod greeting "French" [params]
 "Bonjour!")

;then can use this like this:
(def english-map {"id" "1", "language" "English"})
(def french-map {"id" "2", "language" "French"})

=>(greeting english-map)
"Hello!"
=>(greeting french-map)
"Bonjour!"

こんな具合にメソッドgreetingに対して様々な結果を出力させることができる。

Common LispClojureの違い

Clojureにおけるマルチメソッドdefmultiは、Common Lispだと総称関数defgenericに相当すると思われる(多分)。

Common Lispの場合、このようなマルチメソッドの定義は、

  1. まず最初に総称関数を宣言する
  2. その後にdefmethodでメソッドを生やす
  3. メソッドの分岐はdefmethodの引数の中でそれぞれの引数の型ごとに行う

実はCommon Lispは別に最初にdefgenericなんかやらなくてもdefmethodでメソッドを用意できるのだけれど、今回はClojuredefmultiとの対比を行うためにこんな感じで。

さて、具体的にCommon Lispではどのようにマルチメソッドの定義を行うかというと

(defgeneric greeting (a))

(defmethod greeting ((a string))
  (format t "Hello ~a!~&" name))

(defmethod greeting ((a integer))
  (format t "Your number is ~a." a))

というような感じで、引数の型ごとにマルチメソッドを定義する。

この場合、型は整数型とか意外にも自作の型が使えて、別途でクラスを定義することによって可能となる。

(defclass person ()
  ((name
    :initarg :name
    :accessor name)))

(setf p1 (make-instance 'person :name "Komi"))

(defmethod greet ((obj person))
  (format t "My name is ~a!~&" (name obj)))

こんな具合である。

setfでクラスのインスタンスを作った際にコンパイラ側にメタデータが渡され、それをもとにdefmethodで生やしたメソッドを使う際にメタデータの参照を行っているらしい。

簡単にまとめると

(defgeneric {総称関数の名前} ({引数})

(defmethod {総称関数の名前} (({引数} {引数の型}))
  {関数の中身})

とまあCommon Lispはこのように引数の型でマルチメソッドを定義して利用することができる。

さて、Clojureの場合どうかというと、最初の例をもう一度出すと

(defmulti greeting
  (fn [x] (get x "language")))

(defmethod greeting "English" [params]
 "Hello!")

(defmethod greeting "French" [params]
 "Bonjour!")

(def english-map {"id" "1", "language" "English"})
(def french-map {"id" "2", "language" "French"})

=>(greeting english-map)
"Hello!"
=>(greeting french-map)
"Bonjour!"

という感じ。

Clojureの場合、このマルチメソッドの動きとして

(defmulti {マルチメソッド名} 
  {何でメソッドを分岐させるか})

(defmethod {マルチメソッド名} {分岐のパターン} {引数}
  {関数の中身})

という風になっている。

今回の例では、(fn [x] (get x "language"))によってマップの中からlanguageに対応するものを引っ張ってきて、そして各languageごとに出力を変えている。

今回の例では引数は使っていないが、この定義方法なら各メソッドごとに引数の数や型を自由に指定できるわけである。

Common Lispとの違いとして、Common Lispでの総称関数では最初のdefgenericで各メソッドでの引数を指定していたのに対し、Clojureではdefmultiではディスパッチ(メソッドの分岐)だけを最初に定義しているということらしい。

うーん、結構違う感じだ。

ただ、冷静になって考えてみれば、たしかにClojureでのマルチメソッドの定義方法の方が柔軟な書き方ができるし、こっちの方が良いのではないかという気がしてくる。

実際、何かライブラリを用いる際にテキトーにオリジナルのメソッドを生やすとなるとClojureのやり方の方が使い勝手が良さそう。

Common Lispの場合、何かメソッドを生やす際は雑に何かラップした上でやるか、もしくは別の戦略を取る必要があり、こう考えるとClojureの方が自由度が高いと見ることができる。

プロトコル

マルチメソッドの書き方についてClojure良いなぁなんて思っていたら、どうやらClojureにはプロトコルdefprotocolというのが存在するらしい。

Clojureプロトコルとは、最初に型だけを定義して、メソッドの中身については後から実装していく感じらしい。

(defprotocol Hoge
  (foo [this])
  (bar [this a b]))

(defrecord Hoge1 []
  Hoge
  (foo [this] "this is Test Protocol")
  (bar [this a b] (str a b)))

(foo (Hoge1.))
=> "this is Test Protocol"

(bar (Hoge1.) "a" "b")
=> "ab"

なるほど、こっちの方がオブジェクト指向な書き方だ。

マルチメソッドに比べたらプロトコルの方がカッチリしている。

ところで、なぜかこいつには既視感を感じる。

class Hoge():
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def foo(self):
        return "this is Test Protocol"
    def bar(self, a, b):
        return str(a) + str(b)

あ、完全にPythonじゃん。

なるほどなるほど、まさかこんなところでClojurePythonが似ているなんてちょっと予想外だった。

面白い。

まとめ

今回はClojureのマルチメソッドの定義方法をCommon Lispと比較しながら雑にまとめた。

また、同時にClojureプロトコルについても簡単に説明した。

個人的にはあまりオブジェクト指向な書き方は好きじゃないのだけれど、場合によってはOOPで書いた方が楽な場合もあったりして、そこらへんはある程度妥協しながらということになるのかな。

とりあえず、Clojureおもろいなぁという感想。

これからもがんばるぞい!