There's plenty of excellent answers that cover the unfortunate symptoms of null
, so I'd like to present an alternative argument: Null is a flaw in the type system.
The purpose of a type system is to ensure that the different components of a program "fit together" properly; a well-typed program can't "off the rails" into undefined behavior.
Consider a hypothetical dialect of Java, or whatever your preferred statically-typed language is, where you can assign the string "Hello, world!"
to any variable of any type:
Foo foo1 = new Foo(); // Legal
Foo foo2 = "Hello, world!"; // Also legal
Foo foo3 = "Bonjour!"; // Not legal - only "Hello, world!" is allowed
And you can check variables like so:
if (foo1 != "Hello, world!") {
bar(foo1);
} else {
baz();
}
There's nothing impossible about this - someone could design such a language if they wanted to. The special value need not be "Hello, world!"
- it could've been the number 42, the tuple (1, 4, 9)
, or, say, null
. But why would you do this? A variable of type Foo
should only hold Foo
s - that's the whole point of the type system! null
is not a Foo
any more than "Hello, world!"
is. Worse, null
is not a value of any type, and there's nothing you can do with it!
The programmer can never be sure that a variable actually holds a Foo
, and neither can the program; in order to avoid undefined behavior, it has to check variables for "Hello, world!"
before using them as Foo
s. Note that doing the string check in the previous snippet doesn't propagate the fact that foo1 is really a Foo
- bar
will likely have its own check as well, just to be safe.
Compare that to using a Maybe
/Option
type with pattern matching:
case maybeFoo of
| Just foo => bar(foo)
| Nothing => baz()
Inside the Just foo
clause, both you and the program know for sure that our Maybe Foo
variable truly does contain a Foo
value - that information is propagated down the call chain, and bar
doesn't need to do any checks. Because Maybe Foo
is a distinct type from Foo
, you're forced to handle the possibility that it could contain Nothing
, so you can never by blindsided by a NullPointerException
. You can reason about your program much more easily and the compiler can omit null checks knowing that all variables of type Foo
really do contain Foo
s. Everyone wins.