Update: Christophe Grand has pointed out a flaw in thecase+
implementation below that affects usage of Java enums and classes in code that is AOT-compiled. My bad. I'm working on an update tocase+
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.
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.