Clojure case statement with classes

2019-04-06 20:58发布

问题:

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?

回答1:

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.



回答2:

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.



回答3:

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