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 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.
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:
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!"
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:
(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))))
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):
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!"
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.
Hi Chas, case relies on values which have stable hash codes. Your work around in case+ may not work when AOT-compiled (eg the case with the String class as dispatch value).
Thank you, Christophe, that’s what I was looking for. I should have caught that earlier, as it was staring me straight in the face (I even wrote “the values used for dispatch in
case+
are still resolved at compile-time” :-/). Java enums don’t have stable hashcodes either, which I never caught because I’ve not been using case+ in an AOT’d project yet.References to fields containing stable-hash-code values thankfully work without a hitch (which was my primary motivator in the first place), but ensuring that it works with enums is important to me as well (the only place where
case
is exceeded by Java’sswitch
). Falling back on the dispatch value enums’ ordinals will do (as is done inswitch
– and doing the same with the result of the first expression tocase
), probably raising an exception if all of the dispatch values aren’t enums of the same type.