Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure case statement with classes

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?

like image 562
Chris Avatar asked Aug 19 '12 18:08

Chris


2 Answers

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.

like image 187
ponzao Avatar answered Nov 16 '22 06:11

ponzao


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.

like image 27
Rafał Dowgird Avatar answered Nov 16 '22 07:11

Rafał Dowgird