Search code examples
clojureclojure-java-interop

Is it possible to use Clojure's case form with a Java enum?


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?


Solution

  • 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)))))))