Nil Punning (Or Null Pointers Considered Not So Bad)
Null pointers are considered by their inventor to be a huge mistake. Clojure inherits its null pointer, called nil
in Clojure, from the JVM. In contrast to Java1, Clojure seems to embrace the null pointer. In this post, I'd like to explore how Clojure uses the null pointer in what is often called nil-punning.
Nil-punning has its roots in the very first Lisps, where nil
was both false (the boolean value) and the end of a list (the empty list). It was also often used to represent "no answer", as in what is the first element of an empty list. It is called punning because you can use it to mean different things in different contexts.2
In Clojure, nil
, as a value, is nearly void of meaning. And it is all pervasive, because it can be returned from any Clojure function or Java method.
Let's go through that last part bit by bit.
- It is a value. Java made the mistake of making
null
a lack of object even though it was pointed to by an object pointer. You can't call methods on it. It is not an object. It has a weird nameless type. Clojure did not make this mistake. It is a first-class value and type3, meaning it can be compared to other values, it can implement protocols, and be used as the key or value of a map, etc. Usingnil
where it doesn't make sense in Clojure code is usually a type error, not a NullPointerException, just as using a number as a function is a type error.
- It is nearly void of meaning. It means "no answer", but not much more. Because of this, it can take whatever form fits the context. With proper wisdom in choosing what form it takes,
nil
can become an asset instead of a liability. Clojure takes nil-punning to an extreme.
nil
can be many things. To name but a few, nil
plays false
as a boolean. It plays the empty seq
as a seq
4. It plays the empty map as a map. Because nil
has a role to play in most of the major abstractions of core Clojure, it rarely leads you into an error situation. An unexpected nil can surprise a good programmer, just as much as an unexpected Nothing
from a Haskell function can bewilder even the most experienced Haskeller. 5 Finding out where a nil
came from is the hardest problem that nil
s present.
- It is all-pervasive.
nil
s are normal parts of Clojure programs. They are not anomalous as in Java, where you often have to check it everywhere. This means it is always on the experienced programmer's mind.nil
s flow like water through s-expressions.
first
has nothing to return if the seq is empty, and so it returns nil
. Because nil
is a seq, first
works on nil
, and returns nil
. 6. rest
works on nil
as well, because nil
is a seq.
These examples show the best of nil-punning. When nil-punning works right, nil
s are expected and they give the expected results.
nil
is everywhere, but it can be used mostly everywhere as well--without error and often with exactly the desired result. There are many abstractions that nil
does not participate in (for instance IFn, which is Clojure's interface for things that can be called like functions). In these places, nil
can present a problem--a problem of type, the same as if you tried to call a number as a function.
The best thing to do, in my experience, is simply to wrap the expression in a (when )
to catch the nil
cases, if appropriate, while also preserving it. Otherwise, perhaps letting the Exception bubble up is the best answer. If you got a nil
where you couldn't use one, the stack trace is probably your best clue to where it came from.
After a bit of experience with Clojure, I rarely have difficult problems with out-of-place nil
s in pure Clojure code. However, there is often some Java interop--namely, calling Java methods directly--that will cause a NullPointerException if the object of the method call is nil
. In these cases, wrapping a Java method call in a (when )
is often appropriate. But sometimes not, and the NullPointerException is welcome.
There are some decisions in Clojure that I think make poor use of nil-punning. These places actually make working with Clojure more difficult than they need to be. For instance, (str nil)
is the empty string. Printing this out prints nothing--a form of silence, which is rarely what you want so you have to check for nil
s in these cases. But nil
is not the empty string, like it is the empty seq
. (clojure.string/trim nil)
throws a NullPointerException. This is inconsistent behavior. When nil
acts inconsistently, nil-punning does not work right. nil
s need to be checked. In the worst cases, nil
s fail silently. While I have learned to deal with these situations, they are a wart on the language. The fact that nil
s are so common does help surface the bugs sooner. A small consolation.
Let me make it clear: null pointers are still a costly problem in Clojure. But I can make a claim similar to what Haskellers claim about the type system: nil-punning eliminates a certain class of errors. A fortuitous set of decisions in Clojure has reduced the magnitude of the problem. And some decisions have made the problem worse by hiding it. In general, I find that by embracing nil-punning, my code gets better.
You might also like
- 3 Things Java Programmers Can Steal from Clojure
- Atom code explanation
- Clojure Gazette Looking Forward
- Clojure is Imperative
I don't mean to pick on Java alone. I just wanted to be specific.↩
Note that this is very different from weak typing as you find in Javascript or Python. Nil-punning is more like polymorphism.↩
The type of
nil
isnil
↩Clojure's core is built on several small, powerful abstractions. The most prominent abstraction is seq, which stands for sequence. seq basically has two operations,
first
andrest
. The most obvious use for them is to iterate through items of a collection. There are built-in implementations for lists, vectors, sets, hashmaps, and even strings. But anything that has a notion of sequential values can implement seq, including Java Iterators. I would also like to posit that the most important and often overlooked implementation ofseq
is fornil
itself.↩Even the best Haskellers complain about not knowing where a Nothing came from.↩
You might look at this as nil-preserving behavior--much like the Nothing-preserving behavior of the Maybe Monad↩