Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした