I want to switch on the class of a given object in order to encode it.
(defn encoded-msg-for [msg]
(case (class msg)
java.lang.Double (encode-double msg)
java.lang.String (encode-str msg)
java.lang.Long (encode-int msg)
java.lang.Boolean (encode-bool msg)
clojure.lang.PersistentArrayMap (encode-hash msg)
clojure.lang.PersistentVector (encode-vec msg)
nil "~"
)
)
When I call (encoded-msg-for {})
, it returns No matching clause: class clojure.lang.PersistentArrayMap
What is odd is that putting the cases into a hash-map (with the classes as keys and strings as values) works perfectly well.
Also, (= (class {}) clojure.lang.PersistentArrayMap)
is true. What comparison is happening here and how can I switch either on the class of the object itself or (better) something in its hierarchy?
I believe case
treats the class names as literal symbols - it does not resolve them to actual classes:
>>> (case 'clojure.lang.PersistentArrayMap clojure.lang.PersistentArrayMap 1 17)
1
>>> (case clojure.lang.PersistentArrayMap clojure.lang.PersistentArrayMap 1 17)
17
This is rather unintuitive, but so it works in Clojure's case
. Anyway, the idiomatic way is to use defmulti
and defmethod
instead of switching on type
:
(defmulti encoded-msg class)
(defmethod encoded-msg java.util.Map [x] 5)
(defmethod encoded-msg java.lang.Double [x] 7)
>>> (encoded-msg {})
5
>>> (encoded-msg 2.0)
7
The dispatcher uses the isa?
predicate which deals well with the comparisons of types, in particular it works well with Java inheritance.
If you don't want to use defmulti
, then condp
might replace case
in your use case, as it properly evaluates the test-expressions. On the other hand it doesn't provide constant time dispatch.
If you are dispatching only on the class then protocols might be a nice solution, because they will enable you (or your API's client) to provide implementations for other types at a later time, here is an example:
(defprotocol Encodable
(encode [this]))
(extend-protocol Encodable
java.lang.String
(encode [this] (println "encoding string"))
clojure.lang.PersistentVector
(encode [this] (println "encoding vector")))
If you need to have finer-grained dispatch or you know extending to other types is not necessary then there might be too much boilerplate in this solution.
If you are looking for an alternative way to achieve this, take a peek at condp
:
(condp = (type {})
clojure.lang.PersistentArrayMap :one
clojure.lang.PersistentVector :many
:unknown) ;; => :one