ClojureScript Const Var Inlining
The ClojureScript 1.9.655 release introduced ^:const
Var inlining. This is an exciting new feature—upgrade your compiler and give it a spin! This post will provide some detail on this new feature, covering some of its more subtler aspects.
Review and Definition
Previously, adding :const
metadata to a Var, as in
(def ^:const foo 3)
would have two effects in ClojureScript:
- It prevents re-definition within the same namespace.
- It allows
case
test constants which are symbols resolving to^:const
Vars to represent their values.
Now, with ClojureScript 1.9.655 and later, a new behavior is introduced:
- Compile-time static edn values are inlined.
This means, for example, with the Var foo
as defined above,
(+ foo 12)
effectively acts as if converted to
(+ 3 12)
What is a compile-time static edn value? It can be defined recursively as a value produce by:
- A literal which is any of
nil
, a Boolean, a number, a character, a string, a keyword, a symbol, an#inst
, or a#uuid
. - A (potentially empty) persistent collection literal (any of set, map, vector, or list) containing compile-time static edn values.
As expected, quoted versions of the above are fine. This is essential for creating symbol and list constants, and also allows you to easily create, say, a set collection constant containing symbols.
Examples include 1
, "foo"
, and {:a 'sym}
.
If you define a ^:const
Var initialized with something that is not a compile-time static edn value, then the value will not be inlined (but the no re-definition rule will still apply).
Also note that, while a value could effectively be considered a compile-time constant, this
(def ^:const foo (str "bar" "baz"))
won't be inlined owing to the compilation and evaluation model. The initializer will evaluated in the target JavaScript environment; not at compile time.
While it is tempting to consider having the compiler calculate initializer values for pure functions (producing the constant
"barbaz"
above), this would fail to work properly for hosty values:(+ 16r1FFFFFFFFFFFFF 2)
would produce two different values if evaluated at compile-time vs. runtime for non-self-hosted ClojureScript.
Performance and Behavior
The primary benefit of using ^:const
is avoiding Var indirection and having constants directly embedded in the emitted JavaScript.
You can look at the JavaScript produced in a REPL by using the
:repl-verbose
REPL option. (More: See JS in CLJS REPL)
The JavaScript produced for (+ foo 12)
, where foo
was defined as above is:
((3) + (12))
This can clearly be a win for “small” constants, leading to more compact and direct code.
On the other hand, if you have something like a map value, depending on access patterns, it might be more efficient to not define it as a constant. For example
(def ^:const m {:a 1 :b 2 :c 3})
(defn lookup [] (:a m))
(simple-benchmark [] (lookup) 1e7)
yields a timing of 1629 milliseconds on my machine, but if I leave the ^:const
off, I get a timing of 480 milliseconds.
If you look at the JavaScript produced, you can see that the difference boils down to either constructing a new map value each time lookup
is called (the inlined constant map value), or referring to a single instance of the map via a JavaScript variable. (In effect, leaving ^:const
off, in this case, leads to a result that is somewhat akin what occurs when you use the :optimize-constants
compiler option for keywords and symbols.)
If you'd like to have a
^:const
Var not be inlined, you can defeat inlining by simply wrapping the value with a call toidentity
. For the example above:(def ^:const m (identity {:a 1 :b 2 :c 3}))
.
What about case
? With the new compiler, test constants which are symbols resolving to ^:const
Vars will be inlined with their values, instead of representing their values.
For example, this case
at the beginning of this post compiles down to JavaScript that looks like this:
switch (x) {
case 3:
return 1;
break;
case 4:
return 2;
break;
case 7:
return 3;
break;
case "hi":
return 4;
break;
default:
return new cljs.core.Keyword(null,"no-match",
"no-match",(568148921));
}
Notice that the JavaScript now has case 3
and case 4
instead of case foo.bar.n
and case foo.bar.m
.
Identity
Another difference in behavior that can be observed is with the use of identical?
in the case where inlined constants involve allocation. This code
(def ^:const kw :foo)
(identical? kw kw)
now yields false
, where it would yield true
without ^:const
. But, of course, since keywords and symbols are not interned, keyword-identical?
and symbol-identical?
can be used for efficient comparison, even in the case of different object instances; keyword-identical?
returns true
for the example above.
Caching
One final caution with respect to inlining behavior: If you have a namespace that uses ^:const
Vars from another namespace, and you change the value of such a constant, you my need to ensure that the consuming namespace is re-compiled so that the updated constant value is re-inlined.
For example, let's say foo/core.cljs
contains:
(ns foo.core)
(def ^:const a 1)
(prn a)
and bar/core.cljs
contains:
(ns bar.core
(:require foo.core))
(def b foo.core/a)
(prn b)
If at the REPL, you (require 'bar.core)
you will see 1
printed twice. If you then quit the REPL, change foo.core/a
to be initialized with 2
and then go back into the REPL, you will see 2
printed, followed by 1
.
You could fix this by either deleting your cache, or by touching bar/core.cljs
and re-doing the require
.
Summary
For most existing code making use of :^const
, you need do nothing—things will just run faster. I hope the above gives further insight into how this feature works. I think it is exciting—you should give it a try!