top | item 18588239

NULL: The worst mistake of computer science? (2015)

209 points| BerislavLopac | 7 years ago |lucidchart.com

368 comments

order
[+] porpoisely|7 years ago|reply
NULL can mean and be different things in different domains of computer science. NULL in the database world isn't the same thing in the programming world. In the programming world, null is a result of the system architecture, systems programming, etc. In SQL, NULL is a result "lack of data". There have been debates on whether there should be different types of NULL. A NULL type for "data that is available but we don't have it yet" - like car make and model for a car owner. A NULL type for "data that does not apply" - like car make and model for an adult who doesn't own a car. A NULL type for "data that never applies" - like car make and model for a child. Then you get into the philosophical debate on whether a NULL can ever equal a NULL. Does it even make sense to even think of NULL in terms of equality. How can an unknown entity ever equal another equality? But what if you are just asking "are they both unknown"? Then you probably can think of two NULLs as "equal".

In higher level programming, the consensus seems to be the less nulls the better. Which is why languages like C++, C#, etc are introducing Option-like syntax ( mostly to accommodate the database world and their NULLs ).

NULL exists to solve particular problems in computer science. It can also cause a lot of problems. You can argue it's the best solution and worst mistake depending on the situation.

[+] lalaithion|7 years ago|reply
All of those different nulls can be solved by not having null as a special case of your database specification, but as a first class type construct.

    data MightBeData a = Yes a | Unavailable | NotApplicable | NeverApplicable
[+] davemp|7 years ago|reply
This is hard to reconcile with type theory for me.

NULL, to me, implies and uninhabited type, i.e. there can never be a value with a NULL type. Using null for a "data isn't there, apply, available, etc" seems like an abuse of the type system. I see no reason that the former needs to be supported at the type level. These properties are just responses to queries, not some mystical, uninhabitable oblivion. Unnecessary type features just make verification and learning a language much more difficult.

[+] karmakaze|7 years ago|reply
All this is true. I immediately took it to mean the Tony Hoare invention of Null. That of the zero memory pointer. Other types of null usually have other names with the notable exception being databases. Also Go uses nil for its null which puzzled me for a while then realized its behaviour is different, you can work with them to a point, e.g. len() or append().
[+] bytematic|7 years ago|reply
NULL should mean an intended lack of data. Undefined should be unintended.
[+] bunderbunder|7 years ago|reply
You know who works on a platform with NULL but doesn't have quite so many problems with it? DBAs.

There's some need to draw a distinction between the basic idea of NULL, and the way that NULL has been implemented in most high-level programming languages.

In most RDBMSes, values can't be null unless you say they are. Sometimes explicitly, as in table definitions, sometimes implicitly, when you select a JOIN type. Either way, though, the fact that the developer is in control of when it can and cannot happen means that it always has a knowable meaning. (Or should, anyway.)

The problem with many programming languages is, you're given it whether you want it or not. In a low-level language like C, that's reasonable, because it takes a sensible approach to how it works: Only pointers can be null, and all pointers are nullable for obvious (especially in the 1970s) reasons.

More generally, I'm not going to fault languages from that era for trying it out, because this stuff was new, and things were still being felt out. So I don't really fault Tony Hoare for giving null references a try in ALGOL W.

What seems much more bothersome is high level languages like Java and C# cargo culting this behavior. They could have followed the lead from languages like SQL and let the programmer be in control. They should have. They already throw exceptions when a memory allocation fails, and they allow inline variable initialization, and declaring variables at the point of usage, and composite data types have constructors, so they lack all of (early) C's reasons why ubiquitous nulls were a good idea. They could have, I think quite easily, made nullability optional. At which point it'd have basically the same semantics as optional types from functional programming, so I doubt we'd be worrying about it anymore.

But they didn't.

[+] pdkl95|7 years ago|reply
> NULL is a value that is not a value. And that’s a problem.

The problem isn't NULL, it's languages not enforcing the necessary checks for the "no data" condition. Option can still be NULL ("None" in rust), wrapping NULL in a struct doesn't provide any safety. The safety of Option wrapper types is from the other language features (like rust's "match") and a stricter compiler that forces the programmer to write the NULL check.

NULL would be fine if C required you to write this:

    foo_t *maybe_get_foo(/*...*/) {
        if (/*foo_is_available*/) {
            return foo;
        } else {
           return NULL;
        }
    }

    foo_t *f = maybe_get_foo();
    if (!f) { /*...*/ }   // REQUIRED or compile error
    do_something(f->bar); // only allowed after NULL check
Obviously implementing that requirement would be difficult in C. Languages like Rust were designed with enforcement features (match + None, much stronger type/borrow checking), but lets your have "a value that is not a value".
[+] captainmuon|7 years ago|reply
I've made my peace with null. Null is basically just an implicit

    assert(valid(x))
before every time you call a method on x. Similary, I think of exceptions as explicit "crash-unless-caught" commands.

If you write your program with the "blow up early" mentality anway, or use static checking tools and a bit of discipline, I've found that null looses it's terror.

[+] doubletgl|7 years ago|reply
Isn't there an inherent need in programming to express an explicit "nothing" value? Coming from Python and JS, I never found None/null to be much of a problem. I in fact like the distinction of null and undefined in JS. Using null allows you to distinguish from the accidental undefined.
[+] jmfayard|7 years ago|reply
Progress, it's now a solved problem in modern languages like Rust, Swift or Kotlin See for example: https://kotlinlang.org/docs/reference/null-safety.html
[+] masklinn|7 years ago|reply
I mean it's been a solved problem since the 70s if not earlier, the problem has always been uptake. And that is not solved e.g. C++ recently introduced std::optional, which is not a type-safe version of a null pointer but is instead a null pointer wrapper for value types.
[+] prmph|7 years ago|reply
It is not possible to have a NULL type that works for all situations and has stable semantics.

The issue is, NULL should be a concept, not a value. I see no problem with using sentinel values, so long as they are well designed, and such good design comes with skill and experience, just as with all other aspects of architecture. The quest to have a single value that can be used for all the various possible meanings of NULL, to me, is the root of the problem.

[+] nayuki|7 years ago|reply
I agree with pretty much everything in the article. However, I would give Java a lower score because no one uses java.lang.Optional in practice, and there is too much legacy libraries and application code that cannot or will not be changed. Also, the @NotNull annotation isn't in Java SE; it is made available through various third-party libraries.

A language with a null value can dramatically simplify things for a language designer, though. In the case of Java, we know that every array of objects is initialized to null references. Thereafter, we can construct and assign objects to each slot of the array. Otherwise we run into issues that C++ faces - when we construct the array, the field of every object is uninitialized, so they are potentially dangerous if read or destructed, and need the special syntax of placement new to be initialized. The trick to avoiding null here is to avoid pre-allocating an array, and instead to grow a vector one element at a time. The C++ std::vector<E> is very accessible and performant, whereas Java java.util.List<E> is very clunky to use compared to native arrays.

Another case that gets simplified is object construction. When the memory for an object has been allocated but before the user's constructor code has run, what values should the fields have, assuming that they are observable? In a Java constructor, all fields are initially set to null/0, then you simply assign values to fields in the body code of the constructor. In C++ constructors however, you should initialize fields in the initializer list, and then you have still have the option to initialize fields in the body.

I still think pervasive null values are bad for the programmer (rather than the language designer). Now that I have preliminary experience in Rust, I see that its design is much safer and still practical, so I think this language shows the way forward.

[+] arcticbull|7 years ago|reply
Re: Array Initialization

One approach you can take is the Rusty "hang up a technical difficulties sign" (unsafe) while you mess around with potentially uninitialized memory, which is valid, but places the burden on you as the library writer. Another would be to initialize your array of pointers as an array of Option<Box<T>> pre-filled with None. Due to pointer alignment you can actually optimize Option<Box<T>> by turning it into a tagged pointer (which I believe is what Rust does) so that None == null at the machine level, while the language exposes a safe interface on top. [1]

Re: Object Construction

With object construction in Rust you can either (a) create all fields in advance and specify them at construction [best] (b) use mem::uninitialized() [bad] or (c) create a builder which has optional fields for everything and yields a constructed option via 'a' later [most work].

[1] https://doc.rust-lang.org/std/option/

[+] bluejekyll|7 years ago|reply
It appears that Java will eventually get Value types, which will allow for Optional to be defined on the stack, at least making it actually do something useful.
[+] beardyw|7 years ago|reply
That 'nothing' is inconvenient applies the same in mathematics with zero. Why do we have to have a number we can't divide by? What is 0 to the power of 0? It's a special case we always need to worry about. But its inclusion in the number system is not in question.

And I remember my distress using a financial package being told that my unused zero value still MUST have a currency! My pocket is empty, how can it have a currency? If a farmers field is empty - must I say what it is empty of - cows, sheep, aardvarks?

I think worrying about inconsistency here is worrying about the inconsistency of the world we live in. 'Nothing' is a mysterious thing we need to accept and respect.

[+] userbinator|7 years ago|reply
To someone who has been using Asm and C for decades, these arguments just make no sense. Reading this article reminds me of the arguments against pointers, another thing that's frequently criticised by those who don't actually understand how computers work and try to "solve" problems by merely slathering everything in thicker and thicker layers of leaky abstraction. It's not far from "goto considered harmful" either.

any reference can be null, and calling a method on null produces a NullPointerException.

...which immediately tells you to go fix the code.

There are many times when it doesn’t make sense to have a null. Unfortunately, if the language permits anything to be null, well, anything can be null.

That's not an argument. See above.

3. NULL is a special-case

...because it indicates the absence of a value, which is a special case.

though it throws a NullPointerException when run.

...and the cause is obvious. I'm not even a regular Java user (and don't much like the language myself, but for other reasons) and I know the difference between the Boxed types and the regular ones.

NULL is difficult to debug

Seriously? A "nullpo crash" is one of the more trivial things to debug, because it's very distinctive and makes it easy to trace the value back (0 stands out; other addresses, not so much.) What's actually hard to debug? Extraneous null checks that silently cause failures elsewhere.

The proposed "solution" is straightforward, but if you reserve the special null value to indicate absence then you can make do with just one value instead of a pair, of which half the time half of the value is completely useless. If you can check for absence/null, you will have no problems using Maybe/Optional. If you can't, Maybe/Optional won't help you anyway --- because it's ultimately the same thing, using a value without checking for its absence.

[+] cyphar|7 years ago|reply
> another thing that's frequently criticised by those who don't actually understand how computers work and try to "solve" problems by merely slathering everything in thicker and thicker layers of leaky abstraction

I think that you're being quite unkind. Haskell's Maybe type and Rust's Option types are very far from "leaky abstractions" and were developed by people who definitely understand how computers work. In fact, your description of them appears to indicate that you aren't really sure how they work (None doesn't take up "half of the value") -- the point of typeclasses is that cases where NULL is a reasonable value are explicit and your code won't compile if you don't handle NULL cases. Allowing NULL implicitly for many (if not all) types is where problems lie.

It also appears you're arguing that languages which don't have models that are strictly identical to the von Neumann architecture are "merely slathering everything [with] leaky abstraction". Would you argue that LISPs are just leaky abstractions?

[+] flipgimble|7 years ago|reply
If your whole world is asm and C, then I take it you don't care much about type systems. Bless your heart, you lonely programmer of ephemeral software, may you be employed gainfully fixing your own bugs for decades. The saltiness if mostly for entertainment, please don't take too much offense. For everyone else working at a level of complexity where mistakes are inevitable and costly, types are an essential bicycle for the faulty minds of programmers.

The article is not arguing that we shouldn't express or model the absence of a value. It is arguing that all types having to support "no-value" case leads to error prone code that has historically cost immeasurable amount of money and much credibility and respect. If everything can be null then it takes too much effort to null check every reference, so developers routinely forget or think they know better. Instead it argues that we should model the idea a possible empty values as a separate composable type. Then you can write a large percentage of your code with a guarantee that objects/type/values are never going to be nil, while still handling that possibility in the smaller error checking parts of your code base.

One interesting anecdote is that our team, working in Swift, had to integrate a crash reporting tool and verify that it works. The challenge was that we haven't seen a runtime crash in several months in production.

> A "nullpo crash" is one of the more trivial things to debug

If it happens in your debugging environment in front of your eyes then maybe. Some of us work on software that is used by millions over decades and would never get to see any reports from a majority of crashes.

[+] badestrand|7 years ago|reply
> > There are many times when it doesn’t make sense to have a null. Unfortunately, if the language permits anything to be null, well, anything can be null.

> That's not an argument. See above.

It is actually their best point, IMO. I really like how RDBMS/SQL solve this: fields hold values and you specify beforehand whether they can hold NULLs. The author is right, sometimes it does not make sense for variables to be null-able (think ID fields or usernames) but often it does (e.g. a user's avatar). Being able to indicate that would be a nice idea. C++ for example does that, as `Field x` is not nullable but `Field* x` is.

[+] hesselink|7 years ago|reply
The thing is, you're focusing on when you've detected that there is an issue (a crash). A lot of the issues with NULL are the fact that you can't easily detect if beforehand. It's not indicated in the types, or the syntax. That means that it's incredibly easy for a NULL issue to sneak into an uncommon branch or scenario, only to be hit in production.
[+] Ygg2|7 years ago|reply

   > ...which immediately tells you to go fix the code.
Assuming your code isn't deeply nested. I've seen cases where null was triggered years after code went into production. In that case you have to:

A) Assume value isn't null and have more readable code

B) Litter the code with null checks.

e.g.

    if (a.getStuff().getValue() == "TEST")
becomes

    if (a != null && a.getStuff() != null && a.getStuff().getValue() == "TEST)

Thing with Maybe/Optional you have to check for presence of None, otherwise your code won't compile. Another smart way is what C# did. Integer can't be null. Integer? can be null.
[+] lmm|7 years ago|reply
> ...which immediately tells you to go fix the code.

But which code? The point where you observe the error could be many compilation units away from the code that's broken; it might be in a separate project, or even 3rd-party code.

> ...because it indicates the absence of a value, which is a special case.

Why does it need to be a special case? Is your language incapable of modelling something as simple as "maybe the presence of this kind of value, or maybe absence" with plain old ordinary, userspace values?

> Seriously? A "nullpo crash" is one of the more trivial things to debug, because it's very distinctive and makes it easy to trace the value back (0 stands out; other addresses, not so much.) What's actually hard to debug?

"Tracing the value back" is decidedly nontrivial. And totally unnecessary if you just don't allow yourself to create that kind of value in the first place.

> if you reserve the special null value to indicate absence then you can make do with just one value instead of a pair, of which half the time half of the value is completely useless.

What do you mean? If you're talking semantically, you want absence to be a different kind of thing from a value: it should be treated differently. If you're talking about runtime representation, you can pack an Option into the same space as a known-nonzero type if you want to (Rust does this), but that's an implementation detail.

(Confusing sum types with some kind of pair seems to be a common problem for programmers who haven't used anything but C; sum types are a different kind of thing and it's well worth understanding them in their own right).

> If you can check for absence/null, you will have no problems using Maybe/Optional. If you can't, Maybe/Optional won't help you anyway --- because it's ultimately the same thing, using a value without checking for its absence.

Nonsense on multiple levels. Maybes deliberately don't provide an (idiomatic) way to use them without checking. By having a Maybe type for values that can legitimately be absent, you don't have to permit values that can't be absent to be absent, and therefore you don't have to check most values - rather you handle absence of values that can be absent (the very notion of "checking" comes from a C-oriented way of thinking and isn't the idiomatic way to use maybes/options) and don't need to consider absence for things that can't be absent.

[+] pjc50|7 years ago|reply
The whole point of using type systems is to prevent human errors; a "poka-yoke" for programming.

The great advantage of Maybe/Optional systems is that only some of your references have to use them. You can draw a clear boundary between the parts of the code that have to check everything, and those that can prove it's already been checked.

In assembler we have no real type annotations, but for a long time I've considered trying to design a type-checking structure-orientated assembler.

[+] taco_emoji|7 years ago|reply
> ...because it indicates the absence of a value, which is a special case.

But that's exactly the problem -- it's special, meaning it's only useful for certain situations. An ideal type system would provide compile-time guarantees, rather than having to wait for users to report issues. A type system which A) allows you to define variables as non-nullable and B) requires a null guard before every dereference of nullable variables eliminates this entire class of bug. What on earth is wrong with that?

EDIT:

> Seriously? A "nullpo crash" is one of the more trivial things to debug

Even if this were true [0], wouldn't it be easier if such a crash was just never even possible?

[0] which it's not - all a nullpo stack trace tells you is that something important didn't happen, at some point before this

[+] setr|7 years ago|reply
In general, it's difficult to make the case that runtime errors are preferable over compile-time errors, unless the difficulty to enable compile-time errors is significant. In the case of Optional<> as a language/DB type, I can't imagine much effort involved, except for uptake.

Especially given that it can trivially be optimized out, since the eventual assembly should very well make use of the 0x0 property. But if you can encode the guarantee in your "high-level" language, why would you not want it?

In fact, I'm not sure how anyone could imagine assert(n != null) scattered throughout the codebase is a pleasant situation, unless of course, as most do, you're skipping the safety check for unsafe reasons.

[+] TickleSteve|7 years ago|reply
Completely agree, this is CS theory gone off the deep end...
[+] lalaithion|7 years ago|reply
Option<&T>, in Rust, will compile down to a C pointer to T, with Nothing represented by the null pointer.
[+] nsajko|7 years ago|reply
Also, when you are aiming for full test coverage null dereferences will be caught during testing.
[+] jeromebaek|7 years ago|reply
At least C# has the syntactic sugar to easily check for null references which lets you avoid the horrors of code like `(if s != null && s.length)`. Instead you can type `s?.length`. Never have I appreciated syntactic sugar as much.
[+] ohazi|7 years ago|reply
Other candidates:

- null terminated strings

- machine dependent integer widths

[+] altmind|7 years ago|reply
I came here to write that ASCIIZ is the more costly design decision than null, that not only led to crashing programs, but also to security vulnerabilities, sloppy APIs that cannot handle "binary data" and subpar performance.
[+] DaiPlusPlus|7 years ago|reply
Null-terminated strings are useful for buffering values of unknown length (e.g. copying from a network stream). Length-prefixed strings have issues of their own (e.g. what size length prefix to use, efficient encoding of variable-length integer prefixes, what happens if a length-prefixed string's calculated end-position is outside the process' memory space?)
[+] saagarjha|7 years ago|reply
> null terminated strings

This is mentioned.

> machine dependent integer widths

What exactly do you dislike about this?

[+] edoo|7 years ago|reply
Nulls in strongly typed languages can get rather weird but from a C/C++ perspective it is the same as 0. nullptr is just a correctly casted 0.
[+] masklinn|7 years ago|reply
That's not correct.

In C, the literal "0" is a null pointer constant handled at the compilation stage, but casting a runtime zero is not specified to yield a null pointer (and the address zero can be perfectly valid and usable), nor are null pointers specified to be zero-valued (quite the opposite).

[+] grive|7 years ago|reply
This is only true in C, which is part of why C is less insane than C++.

The two languages should not be conflated anymore.

[+] jcelerier|7 years ago|reply
Actually nullptr is a struct.
[+] saagarjha|7 years ago|reply
nullptr is actually "a prvalue of type std::nullptr_t" or something like that. Since C++11 NULL is the same as nullptr.
[+] pella|7 years ago|reply
Julia Missing Values

"Julia provides support for representing missing values in the statistical sense, that is for situations where no value is available for a variable in an observation, but a valid value theoretically exists. Missing values are represented via the missing object, which is the singleton instance of the type Missing. missing is equivalent to NULL in SQL and NA in R, and behaves like them in most situations."

https://docs.julialang.org/en/v1/manual/missing/index.html

+

"First-Class Statistical Missing Values Support in Julia 0.7"

https://julialang.org/blog/2018/06/missing

[+] ChrisRackauckas|7 years ago|reply
The nice thing about Julia is that it separates `nothing` from `missing`. nothing<:Nothing is a null that does not propagate, i.e. `1+nothing` is an error. It's an engineering null, providing a type of null where you don't want to silently continue if doing something bad. On the other hand, missing is propagates, so `1+missing` outputs a missing. This is a Data Scientist's null, where you want to calculate as much as possible and see what results are directly known given the data you have. The two are different concepts and when conflated it makes computing more difficult. By separating the two, Julia handles both domains quite elegantly.
[+] axilmar|7 years ago|reply
NULL is certainly a mistake, but even more of a mistake is not allowing distinct states in variables.

NULL is just another case of a state of a variable. Other states are 1, 15, 0xffffffff, etc.

That mainstream languages don't handle this is the worst mistake of the computer industry.

[+] rusk|7 years ago|reply
Most languages do. Java will always initialise a non-initialised value to NULL for instance (EDIT or 0 or false for primitives).

It's simply a reality of how computers operate that when you allocate a piece of memory (a variable) it will have something in it that you'll need to clear or initialise.

In this respect, NULL is doing you a favour.

[+] tarkin2|7 years ago|reply
I've been using kotlin and swift. They've partly removed null with the 'maybe' feature.

So instead of calling methods on null objects the methods are just not called if the object is null.

This helps when there's a race condition, and you attempt to call a method on a null object, and then that solves itself by the same code being called again without the race condition.

But a lot of the time if the object is null and the method is not called you still have an error, but it's just not a null pointer error now.

This 'nullless' code is nice in some places, especially with UI lifecycles calling code repeatedly, but other times it just changes the type of error you debug.

[+] sifoobar|7 years ago|reply
Nah. There will always be missing values, no matter how many layers of safety measures we wrap around the fact. Hitting a NULL in C is very unforgiving; but that's just the spirit of C, there are plenty of ways to provide a less bumpy ride in higher level languages.

My own baby, Snigl [0], uses the type system to trap runaway missing values without wrapping. Which means that you get an error as soon as you pass a NULL, rather than way down the call stack when it's used.

https://gitlab.com/sifoo/snigl#types

[+] trophycase|7 years ago|reply
I don't write business software that needs many 9s of uptime so I find null to be fine. Yes, returning null is kind of throwing your hands up but I find that to be kind of the point. It allows my software to fail fast if it does and makes it incredibly obvious where things are going wrong. Generally if a reference has a value of null where it shouldn't, I can pinpoint the location of the bug within a few minutes or even seconds.

IMO it makes the program much easier to reason about compared to returning some sort of empty value and then failing much much later in the program.