Boolean Type Hints in ClojureScript
In ClojureScript, you can annotate a function's parameters or return type using ^boolean
. This post describes the what and why.
First off, its worth saying that you normally don’t need to do any of this. But, if you are working with some performance-critical code, understanding what's going on may help.
Let’s look at what gets generated for this form:
(defn foo [x]
(if x 1 2))
To easily see what is generated when using a REPL, use :repl-verbose true
. (See this post for exposition.)
Here is what we get (edited for whitespace):
cljs.user.foo =
(function cljs$user$foo(x) {
if (cljs.core.truth_(x)) {
return (1);
} else {
return (2);
}
})
See that call to cljs.core.truth_
in there? If you peek inside it, you will see that it evaluates to
x != null && x !== false
But, really, a simpler way to look at it is that it deals with the fact that 0
and ""
are false in JavaScript and true in ClojureScript.
Basically, what you are seeing here is that if
is a checked operation—the wrapper ensures correct behavior, at the cost of a tiny bit of overhead. This checking can be disabled for a block of code if you set!
the special cljs.core/*unchecked-if*
var to true
prior to the code and false
after.
But, there is no need to reach for this low-level compiler directive: The check will be elided if the compiler can infer that if
is being applied to a Boolean value.
Let’s rewrite foo
to hint that x
is a Boolean:
(defn foo [^boolean x]
(if x 1 2))
With this, we now get
cljs.user.foo =
(function cljs$user$foo(x) {
if (x) {
return (1);
} else {
return (2);
}
})
Of course, with this in place, (foo 0)
and (foo "")
will evaluate to 2
, where previously you would get 1
.
An aside: When I originally read Clojure Programming, I mistakenly came away with the impression that (notwithstanding
^long
and^double
type declarations in Clojure) adding type hints to a program would not change its semantics under any circumstance. In my mind I focused only on the performance aspects—avoiding reflection in Clojure.
If foo
is a function that requires a Boolean argument, then (foo 0)
and (foo "")
are incorrect programs. They are passing non-Boolean values to foo
, which requires Boolean values. Another way of saying this is that if you have a correct program, which passes values of the required type to functions, adding type hints will not change the semantics.
Just like you can add a type hint to a function argument, you can also specify a type hint on the return value of a function. If, for example, you look at the source for cljs.core/not
you will see that it is hinted with ^boolean
for the return type. This hint is consistent with the docstring for not
—it is essentially guaranteeing to return a Boolean, and the ^boolean
hint, well…, provides a hint to the compiler that this is indeed the case.
The cool thing about all of this is that the ClojureScript compiler employs type inference. It can essentially propagate these type hints via let
bindings to the place where the values are used. That’s why you oftentimes need not even worry about type hinting in application-level code. All of the predicates in cljs.core
are hinted to return ^boolean
, for example, and all it takes is a bit of this kind of hinting and inference can go a long way.
In addition to the elision of checks for if
, the compiler will also do some nice optimizations for and
and or
, even for arities greater than two. You can end up with and
and or
forms generating nice compact JavaScript short-circuit Boolean expressions involving &&
and ||
. For example,
(defn foo
[^boolean x ^boolean y ^boolean z]
(and x y z))
will generate a function that returns x && y && z
rather than a series of nested if
constructs.
And likewise for not
: In the following definition, a JavaScript call to cljs.core.not
will be avoided and will be simply replaced with !
:
(defn foo [^boolean x]
(if (not x) 2 3))
In summary, if you are dealing with performance critical code, and you have functions that are known to take or return Boolean values, you can add ^boolean
type hints and generate compact, efficient JavaScript.