ClojureScript Case Constants
For quite a while, ClojureScript has supported ^:const
metadata on def
s. One immediate consequence of ^:const
is that it prevents redefintion (and an attempt to do so will trigger a failure at analysis-time).
But, a lesser-well-known consequence of ^:const
is that it allows symbols to instead represent their values in case
constructs, with a few caveats.
An example is the following:
(ns foo.bar)
(def ^:const n 3)
(def ^:const m 4)
(let [x 4]
(case x
n 1
m 2
7 3
"hi" 4
:no-match))
The let
form above will evaluate to 2
.
To understand the caveats, it is first instructive to consider the conditions under which case
will emit a JavaScript switch
under the hood. For example, the following ClojureScript
(let [x 1]
(case x
1 "a"
(2 "z") "b")
"c")
will compile down to JavaScript that looks similar to
switch (x) {
case 1:
return "a";
break;
case 2:
case "z":
return "b";
break;
default:
return "c";
}
If you instead include a keyword as one of the tests in the example above, then the emitted JavaScript will be a series of if
, else
/if
tests in JavaScript.
Now, knowing the above, consider what happens when you use a ^:const
Var: It is simply used as a JavaScript case expression.
If you were to look at the emitted JavaScript for the very first example above that used m
as a test, you would see code that looks like the following as one of the switch
cases:
case foo.bar.m:
return 2;
break;
Also note that no value inlining is occurring.
If we revisited the first example and introduced a keyword test, then the emitted JavaScript will revert to an if
/else
construct, but more importantly, one of the if
s will involve a test for symbol equality, and the match will fail:
(let [x 4]
(case x
n 1
m 2
7 3
"hi" 4
:kw 5
:no-match))
But… the emitted code is valid: This is acting as a “conventional” case
expression, looking for the symbol 'm
:
(let [x 'm]
(case x
n 1
m 2
7 3
"hi" 4
:kw 5
:no-match))
The above evaluates to 2
.
Finally, note that a ^:const
vector won't work as a case
test.
(def ^:const v [5])
(let [x [5]]
(case x
v 1
7 2
"hi" 3
:no-match))
If you look at the emitted JavaScript for this case, you will see that case foo.bar.v:
is embedded in a switch
. This fails for the literal [5]
, evaluating to :no-match
but succeeds if you (let [x v]
…
Similarly, you can't successfully switch using ^:const
keyword Vars.
My takeaway from all of this: Keep it simple. If you use a case
with symbols def
ed to be ^:const
, stick to primitive numbers or strings for your constant values and ensure all of the case
tests are numbers or strings.
Hopefully the above helps you make use of this feature, remaining within the constraints for which it is designed to work, without falling into any of its pitfalls!