Unhinted ClojureScript

March 31, 2016

The ClojureScript compiler propagates type hints. This means that type information can flow and be used in locations downstream from where it is originally hinted or inferred. This is a very good thing, but there may be some rare cases where you'd like to stop the compiler from doing this.




Here is an interesting bit of code to consider:

(defn f [^boolean b]
  (loop [x b]
    (if x
      (recur 0)
      :done)))

What do you think should occur if you evaluate (f true)?

Try it in your favorite ClojureScript REPL. I dare you! Remember that 0 is truthy in Clojure and ClojureScript.

To understand this, you need to delve into Boolean type hints. You also need to understand that, in ClojureScript, type hints on loop symbols are handled in an essentially static fashion.

What's odd in the above example, is that we've violated an assumption the compiler made regarding the type of x. Perhaps the compiler is being too aggressive in this case. Regardless, an additional compiler warning is on the table for this scenario.

Here is a milder case, involving no concerning runtime behavior, but nevertheless, it causes a pesky compiler warning:

(loop [x nil]
  (if (nil? x)
    (recur 1)
    (+ x 2)))

If you evaluate this, you will be duly admonished

WARNING: cljs.core/+, all arguments must be numbers, got [clj-nil number] instead. at line 4

but promptly rewarded with the answer 3. This occurs because the compiler sees the literal nil being bound to x, and inferencing kicks in.

In both of the cases above, you can essentially “turn off” the inferred types by indicating that the symbols in question can be bound to a value of any type.

How? Use ^any:

Revisiting the Boolean example:

(defn f [^boolean b]
  (loop [^any x b]
    (if x
      (recur 0)
      :done)))

Now, (f true) really has quite different behavior. While I'm quite partial to this function, be sure to get back to me.

And, with a well-placed ^any in our arithmetic example,

(loop [^any x nil]
  (if (nil? x)
    (recur 1)
    (+ x 2)))

the compiler will be content with what we are doing.

Is ^any special? It is, in the sense that something like ^foo won't do, especially for the arithmetic example: It would cause the compiler to instead complain about us adding a foo to a number, which won't do.

If you look at the compiler implementation, you will see that ^any is associated with the semantics you'd expect. Is this formally specified anywhere? Not that I'm aware of. It is certainly not well-known like ^boolean, but it works, and I suspect it probably always will. Anyway, that's my 2¢.

Tags: ClojureScript