Typed Clojure prevents Null Pointer Exceptions

04 Oct 2013

I am crowdfunding towards 12 months of full-time Typed Clojure development. If you value strong guarantees like the static prevention of Null Pointer Exceptions, pledge or share today!

Typed Clojure is opinionated: some type errors are checked at compile time, and other checks are delayed until runtime. One kind of type error Typed Clojure is designed to eradicate at compile time is the Null Pointer Exception.

This decision is enforced in regular Clojure code without complicated runtime constructs or special handling. In fact, the design conforms so well to the way Clojure programmers reason about problems they will hardly even notice the type system's pedantry.

nil in practice

To understand how Typed Clojure helps Clojure programmers make such strong claims of their code, we should explore the nature of nil in Clojure.

At its most basic level, nil is exactly Java's null value.

nil has a special property in Clojure: it is a false value. This is particularly convenient for branching in the presence of nil, as tests like (nil? e) to test for nil can simply be replaced by e.

The places where nil is used are very similar to their intended use in Java: a way to express "nothing", or a lack of a value. The crucial detail here is the expressivity of the type system: it's not inherently bad to have a null value, but it's a sin to create a type system that cannot reason about its usage.

This is exactly the situation in Java: null is implicitly part of every reference type, at least according to the type system. This leaves the complicated and error-prone task of correctly handling null to the programmer; infamously, it's all too easy to get it wrong.

nil is explicit

Typed Clojure's handling of nil is easy to understand: nil is just another value, being the only instance of class nil. This may seem we are destroying what makes null a null value: this is the point. It is a false convenience for null to be implicitly included in all reference types.

Now that nil is just another value type, we must be explicit about its usage. Clojure programmers think in terms of data flow. To capture this insight, types must be rich enough to describe arbitrary combinations of data. Typed Clojure provides unions for exactly this: if a function returns a java.lang.Integer or nil the type is simply (U java.lang.Integer nil).

Introducing nil with unions is all well and good, but we need a mechanism to eliminate nil if a potential misuse of nil is to result in a type error.

Typed Clojure already supports a powerful and easy-to-understand way of refining a union: occurrence typing. Typed Clojure knows that the local binding x in (when x e) will never be nil in expression e, and actually updates the type of x without nil. This also extends to nested structures like heterogeneous maps: the :foo key of x in (when (:foo x) e) is known to never be nil in e.

Type checking interactions with Java code is an interesting problem with an explicit nil type, but the solution is intuitive, essentially relying on the programmer to annotate where nil is permitted. The only problem is whether these annotations actually describe the behaviour of the corresponding Java code; if they differ, there is unsoundness in the type checking. Currently, global Java annotations are treated as unchecked assumptions, but we could check these annotations at runtime (and thus fail as early as possible) as part of future work.

The result

Using Typed Clojure, Clojure programmers can rest easy that they are never misusing nil. Programming with the flexibility and conciseness that Clojure provides while enjoying this guarantee is incredibly liberating, and allows you to concentrate on more important things.

You are protected from the sometimes strange world of Java, forcing you to be explicit about the things a Java type annotation lacks. You can research and annotate the exact semantics of your library once, and know Typed Clojure will always catch type errors that might result in a Null Pointer Exception.

History shows having a null value is extremely error prone, and even a single mishandled null value can be catastrophic. We as programmers need all the help we can get null right: Typed Clojure answers this for Clojure programmers.

Pledge towards further Typed Clojure development