36

This comes with a debate with my colleague that I'm using null as an object initial state.

type Value = Node | null
const [value, setValue] = React.useState<Value>(null)

function test(v: Value) {
  if (value === null) // Do something
  else // Do something
}

I'm curious if null is a mistake. How is it solved in languages that don't support null pointers?

6
  • 9
    Sharing your research helps everyone. Tell us what you've tried and why it didn't meet your needs. This demonstrates that you've taken the time to try to help yourself, it saves us from reiterating obvious answers, and most of all it helps you get a more specific and relevant answer. Also see How to Ask – gnat Jul 26 '20 at 22:19
  • 2
    This is not helping, I only know language that support null pointers and I think it's useful in many cases as I demonstrate in my example code. I'm asking to a field I'm not familiar of. – Mengo Jul 26 '20 at 22:24
  • 11
    null is only a mistake if you don't know how to deal with it correctly. One way to deal with it correctly is to make sure all of your object references always have a value. Hence, your colleague's argument. – Robert Harvey Jul 26 '20 at 23:18
  • 1
    I think if null is a built-in concept of your language you cannot get rid of it and have to deal with it. Your language might not offer something like Optionals. But you can aim for making sure that null checking, if it cannot be avoided, will only happen at a single place in your code rather than having null checks on the same object all over the place. For instance, if you have to use some library which will return null you can wrap that library access with your own code, do the null checking there and make sure other parts of your code have to do this as well. – afh Oct 11 '20 at 5:52
  • 1
    And for your own code you can replace return of null with some custom Null Object. For instance, if you have some price calculation logic where you can have discounts or no discount, I often see code with a check for the discount object being null or not in different places. In this example this could be avoided if you have some *NoDiscount" class instead of returning null which will also provide a discount which is simply zero. If this approach is feasible and is worth the (from my experience still small effort in most cases) effort of course depends on your situation. – afh Oct 11 '20 at 5:57
79

The problem isn't null itself. It's the implicit nullability of all object references, as is the case in Java, Python, Ruby (and previously C#, although that picture is changing).

If you have a language where every type T really means T | Null. Suppose you had a system that takes such a nullable reference at its entry point, does some null checking, and wants to forward on to another function to do the "meat" of the work. That other function has no possible way to express "a non-null T" in Java (absent annotations), Python or Ruby. The interface can't encode its expectation for a non-null value, thus the compiler can't stop you from passing null. And if you do, fun things happen.

The "solution" (or "a solution", I should say), is to make your language's references non-nullable by default (equivalently, not having any null references at all, and introducing them at the library level using an Optional/Option/Maybe monad). Nullability is opt-in, and explicit. This is the case in Swift, Rust, Kotlin, TypeScript, and now C#. That way, you clearly distinguish T? vs T.

I would not recommend doing what you did, which is to obscure the nullability behind a type alias.

29
  • 2
    @Mengo I don't know the conventions here, so I can't help. But definitely steer clear of vague nothingness like "Value" – Alexander Jul 27 '20 at 0:58
  • 5
    Good answer!  And coming from a Kotlin background, I'd say that in addition to able to identify nullable types, it's also a Very Good Thing™ for the language to provide easy ways to safely handle nullable values, and for compiler to check for and disallow unsafe null accesses.  (In such a language, nulls are arguably superior to most of the alternatives.) – gidds Jul 27 '20 at 10:07
  • 4
    @Ant Python has None which, while technically an object itself, is functionally equivalent to null in that any undefined objects will default to being None. – Sean Burton Jul 27 '20 at 12:34
  • 3
    @SimonLehmann yeah, good point. It's similar in Kotlin. Great interop with Java APIs, but a lot of libraries aren't annotated with nonnull annotations, so you're really missing out a lot. Having @nonnull and final all over the code base isn't great, either. I've grown to be strongly convinced that languages should default to non-null references by default, and read-only variables by default. Nullability and mutability should be opt-in. – Alexander Jul 27 '20 at 14:21
  • 2
    @PhilFrost Aren't those type annotations for human consumption only, and not actually enforced? – Alexander Jul 27 '20 at 14:42
8

Great question, many developers wouldn't even think or care about! keep thinking and asking!

You are probably looking for optionals. Here, for example, in TypeScript that you use:

http://dotnetpattern.com/typescript-optional-parameters

The idea is to have explicitly state:

  • A) if an object can be null or not
  • B) unwrapping nullable objects before using them to avoid null pointer exceptions (NPEs)

The TypeScript parameter example I linked does not really do B)

if (address == undefined) {
  // Do something with a
} else {
  // Address not found, do something else
}

Maybe you can find a better way in TypeScript like it is done in Swift:

if let a = address {
  // Do something with a
} else {
  // Address not found. Do something else.
}

Or Kotlin:

address?.let { a ->
  // Do something with a
} ?: {
  // Address not found. Do something else
}()

The difference is that in the TypeScript example above you can still forget the check and just address, leading to an NPE. Optionals like in Swift or Kotlin will force you to first check the value existence before you can use them.

A common mistake done then is to force unwarp the value because you think the value always exists:

address!.doSomething() // In Swift

address!!.doSomething() // in Kotlin

If you see code like this, run! Usually a value is an optional for a reason, so just skipping over that safety-measure and claiming "there is a value, I am sure" leads you back to the old NPE-coding we try to get rid of.

A quick search looks like you are not really able to do that in TypeScript without voodoo:

Is it possible to unwrap an optional/nullable value in TypeScript?

7
  • It's good to see how another language address this. Feels in Swift and Kotlin we have to deal with null types. In TS, we can do function(o: A) so null/undefined cannot be pass into this function. – Mengo Jul 26 '20 at 23:34
  • 1
    @Mengo I think you misread something, in Swift or Kotlin, A is guaranteed to be non-null (so long as A isn't a typealias for some type like B?, hiding the optionality like your like your Value example) – Alexander Jul 27 '20 at 1:00
  • 2
    In Swift, if an object reference isn’t nullable then it cannot be nil. Absolutely impossible. And the compiler doesn’t even allow you to check if it is nil. – gnasher729 Jul 27 '20 at 9:31
  • 1
    I think there are some legitimate use cases for force-unwrapping, sometimes you know more than the compiler. But generally this answer explains the concept well. – Mark Jul 27 '20 at 10:32
  • @gnasher729: Languages which don't allow one to check for things which are supposed to be impossible can make it hard to protect code against security exploits. That's fine if the languages will only be used in contexts where they will never receive maliciously-constructed data, but many languages need to be suitable for use outside such sheltered environments. – supercat Jul 27 '20 at 17:25
5

You should never have a non-initialised object (with the single exception of a short interval of time while you are initialising the object). Null specifies the absence of an object.

The problem is not null pointers. The problem is that in older languages 1. You cannot specify that a pointer cannot be null, 2. You cannot specify that a pointer can be null, and 3. If a pointer is null nothing stops you from accessing what it points to, resulting in a crash or worse.

This results in case 1 in unnecessary code to check for null pointers and handling them, and no compiler warning if a null pointer is assigned, in case 2 in incorrect assumptions that a pointer would be non-null when it isn’t, and in case 3 it crashes.

8
  • I'd argue the concept of using pointers is in fact the root of the problem. Pointers should ideally be an implementation mechanism the compiler uses internally in some places for efficiency, not a first-class type that developers have to fumble with manually. In languages that have them, your code will be more reliable the more you can avoid them. – T.E.D. Jul 27 '20 at 14:02
  • @T.E.D I think the problem there is that it's really, really hard to avoid any leaking of that particular abstraction. Certainly in .NET but also TypeScript there are a large number of false positives and false negatives and fun edge cases all around. I really have to look around to see if there's a language that can really guarantee that a non-null reference can never be seen as null. – Voo Jul 27 '20 at 15:16
  • Voo you mean a non-nullable reference cannot be nil? I really, really hope that a non-null reference won’t ever be seen as null :-) in Swift you can ran into trouble if you interface with Objective-C Code returning null Even if declared non-nullable, not if you stay within Swift. – gnasher729 Jul 27 '20 at 15:40
  • @Voo - Well, classic Fortran had no pointers. Most coders used COMMON blocks to perform type conversions or to overlay storage at specific places. Linked structures generally had to be implemented in preallocated arrays using indices (which of course had a lot of the same problems as pointers). – T.E.D. Jul 27 '20 at 16:21
  • There are several more languages I've come across that stylistically actively discourage pointer use though. The first ones that come to mind are Ada and Concurrent C, but I suspect many (most?) functional languages are in this category as well. – T.E.D. Jul 27 '20 at 16:24
4

There are many factors that contribute to null being a "Billion Dollar Mistake". Based on your comments, you're looking specifically at TypeScript and the type of possibly-uninitialized values, so I'll boil it down to two key problems: type safety, and value semantics.

The key - a.k.a., tl;dr

Especially in TypeScript, null-ish values are perfectly valid so long as you assign a simple, clear meaning to it ("no value"), and so long as the language supports this by only permitting null-ish values when the type explicitly allows it.

TypeScript naturally guards against accidental null values.

Others have covered type safety, but the key here is that, in TypeScript, you can include or exclude null and undefined as valid values of a type. At least in strict mode, number, number | null, and number | undefined are all separate types. Types are mathematical sets of valid values, and number only covers numbers. That means that (barring any deliberate breaking of the type system), you don't have to worry about a value unexpectedly being null or undefined. The compiler won't let you accidentally do that. In that regard, the type safety of null-like values is not such a big deal in TypeScript, which just leaves semantics.

So what do null and undefined mean?

In short, they mean "no value" - nothing more, nothing less. The number type can be any number, but it must be a number. In some cases, you want a type that's "a number, or nothing" - that's where you might use number | undefined. If you want something other than "has a value or doesn't" - such as "technically a value, but invalid" or "there was an error trying to compute the value", you need to use something other than null or undefined. The exact solution depends on the exact use case - over-generalizing is what caused the problem in the first place!

So why are there two?

This illustrates the "semantics" problem of the Billion Dollar Mistake quite well! Your colleague was right to point out that, in JavaScript, undefined means "was never defined", and shouldn't be allowed after initialization. That leaves null to mean... anything else that isn't a real value? However, TypeScript is not JavaScript! Believe it or not, the team who creates TypeScript (which is, itself, written in TypeScript) does not use null at all! These experts in the topics of language and especially of TypeScript and JavaScript have decided that null does not provide any value above and beyond undefined. But they do use undefined.

I don't want to misrepresent them, but I believe that this is because they're focusing on what those values mean above and beyond how the JavaScript engines use them. Now, undefined simply means "no value". They don't want to add any more than that, so they don't recognize a difference between that and null.

What does this mean for my example?

There's some missing information about what null is supposed to mean, but you seem to be doing the right thing. However, I would ditch the type Value = Node | null and also switch to undefined. Something like:

const [value, setValue] = React.useState<Node | undefined>()

function test(value: Node | undefined) {
  if (value != undefined) // TS knows `v` is a `Node`, not `undefined`
  else // TS knows `v` is `undefined`
}

Bonus: ? operator vs explicit undefined

You can use ? when declaring a value's identifier to suggest that it's optional. This automatically means it can be undefined (as that is the default for when a value is not provided), but it means more than that. function test(value: Node | undefined) means "test requires one argument, which may be undefined", whereas test(value?: Node) means "test can be called with 0 arguments, or with a Node, or with undefined". It's a small difference, but sometimes you want to say "you must tell me what it is, even if you tell me there's no value", in which case you should avoid ?.

4
  • Interesting approach, but I see null meaning “no value” which is very much different from “undefined” or “unknown value”. – gnasher729 Jul 27 '20 at 15:46
  • This is only true when the strictNullChecks compiler option is set. Most typescript projects will allow null to be assigned to a number. – C.M. Jul 27 '20 at 15:57
  • @gnasher729 Yes, you can assign whatever meaning you want to it. I'm just appealing to the opinions of those who arguably best know the ins and outs of the language. Personally, I find the introduction of a secondary value to be clunky and unnecessary. Rarely do I need the difference between "uninitialized" and "initialized to no value" - but if you do, by all means use null for that. But, if it matters WHY the value is not defined, make that explicit with a better-named value. – TheRubberDuck Jul 27 '20 at 16:24
  • 1
    @C.M. I'm not sure about "most TypeScript projects" - they certainly recommend strict mode. In any case, I sort of called that out: "At least in strict mode, number, number | null, and number | undefined are all separate types." – TheRubberDuck Jul 27 '20 at 16:26
1

Just don't allow null values.

Require initialization when declaring a reference variable and do not allow to assign null to a reference later.

When a developer actually has a legitimate reason to assign a value which represents the absence of a value (rather the exception than the norm in real world software development), then there are two options.

  • They can assign a Null Object. A Null Object is a placeholder object which represents no value, but implements the full interface. So if anyone calls a method or accesses a field of the Null Object, the developer is obligated to implement what is supposed to happen in that case (a no-op, return a placeholder value, or throw a more specific exception).
  • They can use the Optional pattern. An Optional is a type which contains either an actual value or doesn't. The actual value can usually be accessed in various ways, but every way requires to state what's supposed to happen when no value is present (again: do nothing, return a placeholder or throw a specific exception). Programming such a class in such a language would of course be impossible without also implementing a null-object, which you are then not going to use. So you would either require Optionals to be a part of the language syntax (like nullable types in Kotlin) or add some additional syntax which allows you to create nullable types without creating null-objects (like the None type in Rust which can be used as one possible option of a heterogeneous type)
5
  • Optionals can be and often are implemented as library types. No special syntax needed, although some may be used to improve ergonomics. If your language supports tagged unions, implementing optionals is outright trivial. – 8bittree Jul 27 '20 at 18:01
  • @8bittree How would you implement an optional in a programming language which does not allow to assign null to a reference? – Philipp Jul 27 '20 at 19:54
  • Well, if I were to implement it in Rust, I'd probably start with something like this: Enum Option<T> { None, Some(T), }. That happens to be fairly similar to how it's actually implemented in the core library. – 8bittree Jul 27 '20 at 20:21
  • @8bittree None is just null by another name. Rust is pretty smart by making null/None a separate type so there is not much you can do with None except using it in tagged unions. But it's still a null. – Philipp Jul 27 '20 at 21:09
  • Well, sort of. Conceptually, both null and None can indicate that there's no data. But null is special cased. None is just a generic variant with no associated value. And, going back to my original comment, there's no special syntax needed to implement this sort of optional. – 8bittree Jul 27 '20 at 21:35
0

Most of the dangers associated with null stem from the way common C implementations treat it. In languages where attempts to dereference a null pointer are routinely trapped, a null pointer/reference may cause a program to rudely terminate as a consequence of trying to process uninitialized data, but such termination would have been the norm in strongly-trapped languages with or without null. The problem is that the way C handles pointer arithmetic makes it expensive to ensure that code won't inadvertently take a null pointer, apply an offset to it, and end up with a pointer which is meaningless but is no longer recognizable as a null or invalud pointer. Attempting to access such a pointer might shut down a program, but it may also corrupt data without shutting down the program, thus allowing the corrupt data to replace what would have been good data on disk.

7
  • 1
    * invalid – Peter Mortensen Jul 27 '20 at 15:44
  • @PeterMortensen: Do you disagree that in C code inappropriate attempts to use null pointers are likely to result in undetected data corruption, while many languages other than C could trap 100% of attempts to dereference null pointers without any such risk? – supercat Jul 27 '20 at 17:17
  • @PeterMortensen: Although the invention of null pointers predates the invention of the C language, I'm unaware of any evidence that he viewed them as a "billion dollar mistake" until long after C had become popular, and many C programs had malfunctioned in rather "interesting" ways because of how that language processes pointer arithmetic. [Incidentally, the costs of safe trapping of null pointers in C could have been greatly reduced if hardware vendors had included a variation of "lea" which would trap when using a zero base and non-zero offset]. – supercat Jul 27 '20 at 17:22
  • Yes, in C, using bad pointers (be they dangling, wild, null, misaligned, not pointing to the right type or amount of data, ...) can have all kinds of interesting effects. Singling out just null as the root of all evil is a bit much though. Actually, saying C does anything wrong there just means someone chose the wrong language for their project. – Deduplicator Jul 27 '20 at 17:34
  • @Deduplicator: The problem with C, compared with other languages, is the use of pointers to simultaneously encapsulate arrays and offsets therein. In a language like Pascal, if one wants a function to operate on a range of an array, one would need to pass a pointer to the array, along with integers identifying the starting element and either the count, last element, or just-beyond element. Code using such a triple would then index the unmodified pointer, allowing the compiler to null-check it. In C, it would be much more common to have the caller... – supercat Jul 27 '20 at 17:38
0

Dealing with temporary nullity during initialization is not yet a solved problem, but there are solutions. Kotlin has a few options built in to the language. One is to declare a member variable lateinit. This allows one to run one's initialization process with more flexibility. Under the hood, the variable is actually null at first, but later, null checks are not necessary.

Another technique is to delegate a member property (either variable or constant) to a Lazy object, which is accessed when the member property is accessed. Lazy wraps a value, and when accessed, blocks until it's available. The Lazy class is in the standard library, but the by keyword (which declares the delegation relationship) is language-level, and allows for you to use a class other than Lazy, so this option might be good for cases where you need a lot of flexibility in your initialization process.

Although these answers are Kotlin-specific, they are patterns that can be used in other object oriented languages.

-3

Nullability is not the billion-dollar mistake. Useless programmers who don't validate inputs, are.

Languages or programs that attempt to "solve" the nullability "problem" by introducing monads or optionals or whatever they want to call them, are merely obfuscating the issue of lacking input validation behind an extra layer of abstraction. Essentially, they're trying to do useless programmers' jobs for them.

But that doesn't actually help anything at the end of the day, because a program that doesn't validate its arguments is going to misbehave regardless of whether it receives null or MonadOrOptionalOrMaybeOrWhateverNameYourLanguageThinksIsCool<MyClass>.Default. At least with nullability, you'll get a crash or at the very least a distinctive stack trace; with a monad, your app just won't do what you were expecting it to. Which one of these cases is easier to diagnose and fix?

Ensuring your program runs correctly is your responsibility as the programmer, not the language's. Verify your damn inputs.

3
  • 3
    Calling people names does not make a good answer. Aside from that, I'm seeing a misunderstanding about "MonadOrOptionalOrMaybeOr...". For one, "monad" is a general term that refers to more than the optional/maybe pattern. But it does more than push the issue aside, but brings it to the forefront. If something is passed in as a "maybe" type, you know that you still have to "verify your damn inputs", whereas if it's passed around as the underlying type, that verification has clearly already been done. – TheRubberDuck Jul 27 '20 at 17:35
  • 1
    In my experience, the program with the Optional is vastly easier to diagnose and fix, because the compiler brings the problem to my attention before the program ever runs. No need to hope and pray that I'll get just the wrong test data to trigger that NPE before it makes it out to production, freeing me up to think about more important things, like what to do in the case of not having data, or what to do with the data when I do have it. – 8bittree Jul 27 '20 at 18:17

Not the answer you're looking for? Browse other questions tagged or ask your own question.