Javaのライブラリを使っていて
型指定の多重ディスパッチが欲しくなったのだけれど
(defmulti test1 (fn ([x] [(class x)])))
(defmethod test1 [String] [x] "OK"))
(defmulti test2
(fn
([x] [(class x)])
([x y] [(class x) (class y)])
([x y z] [(class x) (class y) (class z)])))
(defmethod test2 [Number] [x] (println "Number" x))
(defmethod test2 [nil nil] [x y] (println "Both nil!"))
(defmethod test2 [String nil] [x y] (println "String" x "Nil" (quote y)))
(defmethod test2 [String String] [x y] (println "String" x "String" y))
(defmethod test2 [String Number] [x y] (println "String" x "Number" y))
(defmethod test2 [Number Number] [x y] (println "Number" x "Number" y))
(defmethod test2 [Number Number Number] [x y z] (println "Number" x "Number" y "Number" z))
defmulti/defmethodを毎回書くのは非常に面倒くさい!美しくない!
こんな時こそマクロの出番やで
defmultiとdefmethodをラップしたdefstrictマクロを書いてみた
目標
Common Lispのような型での多重ディスパッチを目指すべし
(defstrict test1 [String x] "OK")
(defstrict test2
([Number x] (println "Number" x))
([nil x nil y] (println "Both nil!"))
([String x nil y] (println "String" x "Nil" 'y))
([String x String y] (println "String" x "String" y))
([String x Number y] (println "String" x "Number" y))
([Number x Number y] (println "Number" x "Number" y))
([Number x Number y Number z] (println "Number" x "Number" y "Number" z)))
実装
既に気づいてる漏れもあるけど簡単な実装ということでスルー
(defn- resolve-class? [sym]
(if (= 'nil sym)
true
(class? (resolve sym))))
(defn- typed-args [args]
(let [s (group-by resolve-class? args)]
{:types (s true), :syms (s false)}))
(defn- signature [sig]
{:args (first sig), :body (rest sig)})
(defn- signatures [sigs]
(map signature sigs))
(defn- multi-signature [sig]
(let [args (or (-> sig :args typed-args :syms) [])
body (vec (map (fn [sym] `(class ~sym)) args))]
`(~args ~body)))
(defn- multi-signatures [sigs]
(map (comp multi-signature first)
(vals (group-by #(count (first %)) sigs))))
(defn- method-signature [sig]
(let [args (-> sig :args typed-args)
types (or (args :types) [])
syms (or (args :syms) [])
body (-> sig :body)]
`(~types ~syms ~@body)))
(defn- method-signatures [sigs]
(map method-signature sigs))
(defmacro defstrict* [name & sigs]
(let [sigs (signatures sigs)
multi-sigs (multi-signatures sigs)
method-sigs (method-signatures sigs)]
`(do
(defmulti ~name (fn ~@multi-sigs))
(remove-all-methods ~name)
~@(map (fn [s] `(defmethod ~name ~@s)) method-sigs))))
(defmacro defstrict [name & sigs]
(if (vector? (first sigs))
`(defstrict* ~name ~sigs)
`(defstrict* ~name ~@sigs)))
使う
目標で書いたものを使って定義してやった後…
user> (test1 "X")
"OK"
user> (test1 10)
IllegalArgumentException No method in multimethod 'test1' for dispatch value [java.lang.Long] clojure.lang.MultiFn.getFn (MultiFn.java:160)
user> (test2 10)
Number 10
nil
user> (test2 nil nil)
Both nil!
nil
user> (test2 "X" nil)
String X Nil y
nil
user> (test2 "X" "Y")
String X String Y
nil
user> (test2 "X" 10)
String X Number 10
nil
user> (test2 10 11)
Numberp 10 Number 11
nil
user> (test2 10 11 12)
Number 10 Number 11 Number 12
nil
user> (test2 10 11 "12")
IllegalArgumentException No method in multimethod 'test2' for dispatch value [java.lang.Long java.lang.Long java.lang.String] clojure.lang.MultiFn.getFn (MultiFn.java 160)
ちゃんと指定した型通り動いた
感想
やっぱりマクロ楽しい!
まぁnilは型のところに入れられるようにしたのだけれどもっといい記述方法ないものか
あと特定の型指定しない場合はdefmulti/defmethodに習って:defaultとか?
追記 2014/3/17
真面目に修正してみてる版はこっち
https://github.com/emanon-was/cljsoup/blob/master/src/cljsoup/macros.clj