ClojureScript Case Constants

June 15, 2015

For quite a while, ClojureScript has supported ^:const metadata on defs. 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 ifs 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 defed 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!

Tags: ClojureScript