10

The case doc says

Unlike cond and condp, case does a constant-time dispatch... All manner of constant expressions are acceptable in case.

I would like to benefit from case's constant-time dispatch to match on Java enums. Java's switch statement works well with enums, but doing the following in Clojure:

(defn foo [x] 
   (case x 
      java.util.concurrent.TimeUnit/MILLISECONDS "yes!"))

(foo java.util.concurrent.TimeUnit/MILLISECONDS)

Results in: IllegalArgumentException No matching clause: MILLISECONDS

Are enums not supported in case? Am I doing something wrong? Must I resort to cond or is there a better solution?

pron
  • 1,693
  • 18
  • 28

3 Answers3

7

The problem here is that case's test constants, as described in the docs, " must be compile-time literals". So, rather than resolving java.util.concurrent.TimeUnit/MILLISECONDS, the literal symbol 'java.util.concurrent.TimeUnit/MILLISECONDS is being tested against.

(foo java.util.concurrent.TimeUnit/MILLISECONDS) ; IllegalArgumentException
(foo 'java.util.concurrent.TimeUnit/MILLISECONDS) ; yes!

Instead, the solution is to dispatch on the .ordinal of the Enum instance, which is what Java itself does when compiling switch statements over enums:

(defn foo [x]
  (case (.ordinal x)
    2 "yes!"))

You can wrap this pattern in a macro which correctly evaluates the case ordinals for you:

(defmacro case-enum
  "Like `case`, but explicitly dispatch on Java enum ordinals."
  [e & clauses]
  (letfn [(enum-ordinal [e] `(let [^Enum e# ~e] (.ordinal e#)))]
    `(case ~(enum-ordinal e)
       ~@(concat
          (mapcat (fn [[test result]]
                    [(eval (enum-ordinal test)) result])
                  (partition 2 clauses))
          (when (odd? (count clauses))
            (list (last clauses)))))))
llasram
  • 4,417
  • 28
  • 28
Beyamor
  • 3,348
  • 1
  • 18
  • 17
  • 3
    cemerick has a write-up and workaround for this at http://cemerick.com/2010/08/03/enhancing-clojures-case-to-evaluate-dispatch-values/ – A. Webb May 27 '13 at 17:53
  • Thanks, both of you! Both the ordinal solution (I'm using this in a macro so readability isn't an issue) and cemerick's solution work well. – pron May 27 '13 at 18:06
  • I was too rash: apparently cemerick's case+ doesn't work for enums: `(case+ java.util.concurrent.TimeUnit/MINUTES java.util.concurrent.TimeUnit/MINUTES "yes!") ; CompilerException java.lang.RuntimeException: Can't embed object in code`. (but the ordinal solution does, obviously) – pron May 27 '13 at 18:43
  • This loses the benefits of using an Enum because you have to know the ordinal value of the Enum, which might change. Not only might it change, you can easily accidentally put the wrong number since the number usually has no relation to the semantics of the Enum. I believe @Federico Tomassetti's answer is the best for this question. – Jason Jan 08 '16 at 16:11
  • Is there a way without using `eval`? Maybe using `.name` or `.hashCode` instead of `.ordinal`, and generating the clauses ahead of generating `case`? Or ultimately by copying the code of `clojure.core/case` maybe? – nha Oct 15 '19 at 16:07
6

You could use use a cond on the name of the enumm

(case (.name myEnumValue) "NAME_MY_ENUM" (println "Hey, it works!"))

Seems to me very simple compared to the alternatives

Federico Tomassetti
  • 2,100
  • 1
  • 19
  • 26
  • This answer should be the accepted answer. It maintains the constant time performance of `case` and preserves the semantics of `Enum`. It does lose the compile time check for the `Enum`, but compile time checks are commonly traded in Clojure for other benefits. – Jason Jan 08 '16 at 16:13
0

Here's a simpler solution that just uses equality checking on the cases -

(defn cases [v & args]
  (let [clauses (partition 2 2 args)]
    (some #(when (= (first %) v) (second %)) clauses))) 

=> (cases EventType/received EventType/send "A" EventType/received "B")
=> "B"
Steve B.
  • 55,454
  • 12
  • 93
  • 132
  • That's not a constant-time operation. The question asked for a constant-time solution. – pron May 21 '15 at 03:52