Enhancing Clojure's case to evaluate dispatch values

Update: Christophe Grand has pointed out a flaw in the case+ implementation below that affects usage of Java enums and classes in code that is AOT-compiled.  My bad.  I'm working on an update to case+ that will address the problem for enums – classes will likely revert to being unsupported.

For those that aren't familiar, Clojure's case macro is an über-switch statement that allows one to dispatch on any compile-time literal in constant time. This includes essentially all scalars (strings, keywords, symbols, numbers) and "composites thereof" (as the docs put it): literal sets, maps, and vectors. The latter feature pushes case a fair ways into pattern-matching territory.

Brief factoid interlude

Clojure's defrecord would not exist as we know it without case, which backs the implementation of defrecord's keyword lookup mechanism.  Without that, records would be without the congruence with regular maps that they currently enjoy: each slot access would either need to run through get (e.g. (get some-record :slot-name)) or direct field access would be required (which would rule out function composition with keyword "accessors", the use of the -> and ->> families of threading macros, etc).

The only downside of case is that it doesn't evaluate the dispatch values that are provided. Only compile-time constants are matched, so if one has values that are defined in a var, accessible as (almost always static) Java class fields, or Java enums, you can't use them as case dispatch values:

[sourcecode] user=> (case javax.swing.JFrame/NORMAL javax.swing.JFrame/NORMAL "normal!") java.lang.IllegalArgumentException: No matching clause: 0 (repl:78) user=> (case 'javax.swing.JFrame/NORMAL javax.swing.JFrame/NORMAL "normal!") "normal!" [/sourcecode]

As you can see, the dispatch value referring to the NORMAL static int field is being matched as a symbol, rather than the field it names. The same thing happens when attempting to refer to Java enums, values held by vars, classes, and so on. This is a problem insofar as I really don't want to fall back to the sequential dispatch of cond or condp, but I need to use dispatch values that are more than just compile-time literals. My current solution is the poorly named case+ macro:

[sourcecode language="clojure"] (defmacro case+ "Same as case, but evaluates dispatch values, needed for referring to class and def'ed constants as well as java.util.Enum instances." [value & clauses] (let [clauses (partition 2 2 nil clauses) default (when (-> clauses last count (== 1)) (last clauses)) clauses (if default (drop-last clauses) clauses) eval-dispatch (fn [d] (if (list? d) (map eval d) (eval d)))] `(case ~value ~@(concat (->> clauses (map #(-> % first eval-dispatch (list (second %)))) (mapcat identity)) default)))) [/sourcecode]

With this available, the interop love flows like butter, and all of the dispatch literals already supported by case are unaffected because they all evaluate to themselves (FYI, Direction is a pre-existing enum class of mine):

[sourcecode] user=> (case+ javax.swing.JFrame/NORMAL javax.swing.JFrame/NORMAL "normal!") "normal!" user=> (case Direction/East Direction/East "east!") user=> java.lang.IllegalArgumentException: No matching clause: East (repl-1:88) user=> (case+ Direction/East Direction/East "east!") "east!" user=> (case String String "it's a string class") java.lang.IllegalArgumentException: No matching clause: class java.lang.String (repl-1:90) user=> (case+ String String "it's a string class") "it's a string class" user=> (def some-constant 42) #'user/some-constant user=> (case 42 some-constant "Towels for everyone!") java.lang.IllegalArgumentException: No matching clause: 42 (repl-1:93) user=> (case+ 42 some-constant "Towels for everyone!") "Towels for everyone!" [/sourcecode]

The big caveat is that the values used for dispatch in case+ are still resolved at compile-time – it is a macro, after all.  So, you can't put case+ in a let form and dispatch on a let-bound value (attempting to do this will lead to an error, as locals cannot be eval'ed).  Dispatch only on values available at compile-time, such as classes and class fields (which include enum instances).

I've used this extensively with good results (case+ accounts for perhaps a 50% of my total case usage), but I am just slightly concerned about my doing an end-around the key behaviour of case in not evaluating dispatch values. I didn't happen to be paying attention in irc when the case design discussions were ongoing, and I don't yet fully understand the motivation of the restriction. So, I offer this up somewhat tentatively for now – your mileage may vary. Do pipe up if you find a situation were case+ falls down.