LoginSignup
6
4

More than 5 years have passed since last update.

Clojureで型指定の多重ディスパッチを書いてみた

Last updated at Posted at 2014-02-25

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

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4