Did you know about type hints?

Did you know, you don't need type hints? No! Really! You don't need them. There is only one situation where you might need them: on call sites for host interop.

Clojure is dynamic, you know?

Clojure is dynamically typed. That doesn't mean that things don't have a type, but that the compiler doesn't check it at compile time.

Now, there's a problem here. The JVM hardwires the method to call in the byte code. So it needs to know the type of things, doesn't it?


Clojure solves this problem by using the IFn interface. It describes callable things—mostly functions. The latter are represented as lists in Clojure. Under the hood they are translated into a call to the .invoke method of the function object.

user=> (defn foo [] :hello)
user=> (foo)
user=> (.invoke foo)

By implementing IFn also other types may take part in “function” calls. Most prominently are Clojure's data structures.

user=> ({:foo :bar} :foo)

However, IFn defines all arguments of .invoke to be of type Object. So you can pass anything you like to a Clojure function. It doesn't matter which type it is, because everything is an Object anyway. (Assume no primitive numerics for the moment.)

So hinting the arguments to a Clojure function never makes sense.


This whole approach works, because the first element of a list is known to be a IFn. So the byte code is hardwired for this interface. When you pass in a wrong type you get a cast error.

user=> (4711)
ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn

But what if you want to do some host interop? Then things are not as clear and clearly depend on the context.

Sometimes Clojure is able to determine the type of a variable. One such situation is when the instance is created in the local environment.

user=> (let [x (String. "foo")]
         (.indexOf x "o"))

A situation where this information is not available are function arguments. As detailed above they are always of type Object. So Clojure has no chance of knowing the type up front.

user=> (defn foo [x] (.indexOf x "o"))
Reflection warning, REPL:3 - call to indexOf can't be resolved.

So the compiler has to resort to reflection which is of course slow. So here it would make sense to hint x with String, so that the compiler can avoid reflection.

BTW: The reflection also depends on the other method arguments. It is not sufficient to hint the method target.

user=> (defn foo [^String x o] (.indexOf x o))
Reflection warning, REPL:4 - call to indexOf can't be resolved.

A method with two different signatures needs all arguments to be hinted properly.

But it doesn't hurt, does it?

So one might think: “It doesn't help, but it doesn't hurt either, so I sprinkle hints all over the place to give myself some info on what this function returns.” And indeed this thinking is wrong.

By now it should be clear that type hints are a low-level construct. Using them in the above mentioned way over specifies the types the functions take and return. You basically lock the code which could in theory be host independent into one platform.

Take for example str. It is hinted to return a String. Is this wrong? No. It uses a host dependent way to efficiently create strings. So you need a custom str version a on different host anyway, which may then be hinted with a different type.

Take for example the following str-cat function:

(defn ^String str-cat
  [& xs]
  (apply str xs))

Is the type hint wrong? Yes! This function would work on any host in the same way. But by needlessly hinting it, we lock it into the JVM.


Only add type hints when you really need them. That is in host interop call sites. And even then you should check, that your really need them.

Sometimes you may even choose to go with the reflective call. But beware the library author. Others will have to live with it!

*warn-on-reflection* is your friend.

Just adding type hints doesn't turn Clojure magically into a statically typed language. Type hints are not Typed Clojure.

Published by Meikel Brandmeyer on .