31 comments

[ 3.1 ms ] story [ 52.5 ms ] thread
> If you want to make pointers not have a nil state by default, this requires one of two possibilities: requiring the programmer to test every pointer on use, or assume pointers cannot be nil. The former is really annoying, and the latter requires something which I did not want to do (which you will most likely not agree with just because it doesn’t seem like a bad thing from the start): explicit initialization of every value everywhere.

That doesn't sound right: the third option is making the compiler check that only initialized memory is read.

I don't believe it is possible (or even makes sense) to completely avoid illegal/invalid objects and structures, and a cheap way to mark these are nil pointers. From that perspective, it doesn't matter to me whether a language allows nil pointers or doesn't have them. What matters are the details, though. It is generally desirable to keep invalid objects properly typed and languages with nil sometimes have quirks with "nil typing" (I'm not sure if that's the right term).
> If you want to make pointers not have a nil state by default, this requires one of two possibilities: requiring the programmer to test every pointer on use, or assume pointers cannot be nil. The former is really annoying, and the latter requires something which I did not want to do (which you will most likely not agree with just because it doesn’t seem like a bad thing from the start): explicit initialization of every value everywhere.

To me this is the crux of the problem with null. It's not that null itself is a problem, it's that nothing communicates when it can be expected. This leads to to anti-patterns like null-checking every single pointer dereference. What's need is a way to signal when and when not `null` is valid under your domain model. This is precisely what stuff like `Option` does. Without a signal like this, programming feels like a minefield, where every dereference is liable to blow up, personally I'm done with that kind of programming.

The latter part of the post about individual-element vs grouped-element mindset is interesting, but I'm not sure it refutes the need for null or reasoning about it.

EDIT: It's also worth noting that Rust can still zero initialise entire structs despite element-wise initialisation when the valid bit pattern for the struct is 0.

I like giner bill, but:

- this is experienced/smart person forgetting about selection bias - yea null ptr errors are easy when you've had years to learn to avoid them.

- systems programmers are just as bad as web devs in forgetting that that their style of programming doesn't always generalize across multiple domains.

- I don't think the article puts enough emphasis on how big of a difference tooling makes; compilers are smart enough now to tip off about lots of errors that manifested as null pointer errors - like forgetting to initialize a variable, that's just a lint error now.

- I also don't understand the obsession with being so strongly tied to C. Like I understand carbon, because that's a direct replacement that google is working on and because c++ has the opposite problem of having too much such that there's a really great language somewhere in the mess, but people aren't seriously going to jump from c to odin. People that like c, use c, are going to stay with c. People that try new languages want something at least a little bit better. The outcome could be the same, but it should be because a feature is good and not as a compromise in the hopes that it'll lure people over.

I agree that null pointers have never been this huge problem compared to many others.

But I think it is rather convenient to formally specify which pointers can be null and which ones can not.

How do I tell the users of my library wether they can pass null into my API? How do I stop people from putting "just in case" null checks into code that doesn't need it, obscuring the logic of the program and burdening callers with yet more error handling?

In my opinion, the "modern" approach of using some sort of Maybe type is just far more productive.

After using TypeScript I see userland Maybe types as a workaround for a language design flaw. When the builtin type system allows you to declare nullable and non-nullable reference types and produces compile errors when you don’t check a nullable value before dereferencing it, the problems with null go away
Option<Option<T>> works fine, T | undefined | undefined doesn't. Typescript does weird things because they wanted to make it backwards compatible with Javascript.
After reading all that (ok skimming), he does mention nullable types, but doesn't go into them at all. What's wrong with having the option of defining if something can be null or not? This is how I understand nullable types, don't known about monads. You only have to check if it's null if you have defined that it can be null.

To me this sounds like a solved problem, for example like how it's done in kotlin: https://kotlinlang.org/docs/null-safety.html#nullable-types-...

Well, probably trillion dollar one by now; It's not just the null pointer bugs that waste time, it's the fact language is designed around them that's the problem

> null pointer dereferences are empirically the easiest class of invalid memory addresses to catch at runtime, and are the least common kind of invalid memory addresses that happen in memory unsafe languages.

and yet I still see them popping up in memory-safe languages

> In statically typed compiled manual-memory managed languages, it’s not as much of a problem empirically compared to the set of invalid memory address problems, of which most of them are solved with a different mindset.

that's moving goalpost away from original claim; it wasn't "billion dollar mistake" in context of ALGOL, it was one because oh so many other languages, static or dynamic, managed or unmanaged, copied that behavior

I don’t understand the premise: null references (of whatever flavor) can simultaneously be trivial to detect at runtime and have been a poor design decision. The two aren’t mutually exclusive.

I think Hoare’s own quote has exactly that nuance in it: reliability and predictability are given as much prominence as security.

I really didn't like this article.

Using scare quotes everywhere just makes it read like the author is engaging in bad faith. And I don't think they really address the issue.

The discussion in the "The Problem of the Individual-Element Mindset" section seems fairly arrogant, and ignorant of the economic realities of why people don't use manual memory management. "Individual-Element code" is not stupid, as they claim, but optimizing for other criteria than performance.

Their core arguments seem to be 1) I don't want to program in a way that excludes null pointers and 2) non-nullable references preclude arena-based memory management.

Regarding 1) you cannot make any useful statements. Their preference is their preference. That's fine and it's a fair argument as far as I'm concerned; they can create the language they want to create.

Regarding 2), you can easily distinguish nullable and non-nullable references in the type system. At the more experimental end are type systems that address these problems more directly. OxCaml has the concept of modes (https://oxcaml.org/documentation/modes/intro/) that track when something has happened to a value. So using modes you can track whether a value is initialized, and thus prevent using before initializing. Capture checking in Scala (https://docs.scala-lang.org/scala3/reference/experimental/cc...) is similar, and can prevent use-after-free (and maybe use before initialization? I'm not sure.) So it's not like this cannot be done safely, and I believe OxCaml at least is used in production code.

The section "The Problem of the Individual-Element Mindset" bugs me quite a bit, the core of it being:

> This architectural mindset does lead to loads of problems as a project scales. Unfortunately, a lot of people never move past this point on their journey as a programmer. Sometimes they do not move past this point as they only program in a language with automatic memory management (e.g. garbage collection or automatic reference counting), and when you are in such a language, you pretty much never think about these aspects as much.

Billions of dollars worth of useful software has been shipped in languages with garbage collection or ARC: roughly the entire Android (JVM) and iOS (ARC) application ecosystems, massively successful websites built on top of JVM languages, Python (Instagram etc.), PHP (Wikipedia, Facebook, ...).

In game development specifically, since there's a Casey Muratori video linked here, we have the entire Unity engine set of games written in garbage-collected C#, including a freaking BAFTA winner in Outer Wilds. Casey, meanwhile, has worked on a low-level game development video series for a decade and... never actually shipped a game?

Runtime errors are much, much worse than compilation errors and that's why it's such a concern.

There is a dev cycle of local dev, CI, deploy to staging, deploy to production. The later you catch an error, the most expensive it is to fix.

Compilation errors are usually caught during local dev, or occasionally CI (if it's in a module that the dev didn't try to build locally).

A runtime error might not be caught until it reaches production, which not only causes a customer impact but now needs an emergency hotfix to push through the whole pipeline which can be a long process for big companies.

The problem with NULL is that it is forcibly adjoined as a value the compiler considers valid to every pointer type. In the languages in question, you can't have a non-nil pointer, no matter how much you might want one.

I do agree that it is, on its own, not necessarily the large problem it is made out to be. One nice thing about NULL dereferences is that they tend to be visible and obvious. They crash things and make lots of noise. As invalid values go, they're actually on the friendlier side. The invalid values that silently sail through your program, corrupting everything they touch, but never crashing anything, are far more dangerous. In fact one of the worst possible solutions to a NULL is to default it to some value, especially one like "0" that looks like half the other values in your program.

The dangers of NULL are somewhat oversold because they are cognitively-available dangers. We see the crashes. We see the failures. But those are far better than the failures we don't see, and the crashes we see only much later, sometimes years later. (Don't ask me about the legacy billing systems I've been trying to deal with.)

But was it a billion dollar mistake? Still yes, because having a type that forces this value on to the legal set is still a mistake and has still caused lots of problems that could have been avoided. In addition to that billion dollar mistake, there has also been a lot of code written that has made similar mistakes of allowing similarly illegal values representable in the code, and that has also been a huge mistake. Perhaps a larger one, perhaps not. Hard to get an objective measure of these two mistakes, both huge, both frequent, both highly impactful.

Ultimately, though, the NULL pointer is just a particular instantiation of the general problem of allowing illegal states to be represented, and of that class of problem, it is probably one of the least harmful examples of it... unless you overly avail yourself of all of those "a?.b?.c?.d" operators and "upgrade" your NULL issues into something that sprays illegal and/or incorrect values through your code instead. I really don't like those things being so convenient to hand, they encourage people to jump from the frying pan into the fire too often.

I really like languages with non-/nullable types like Kotlin and Rust! I used to work with Java for over a decade and therefore used to code very defensively against NullPointerException and now (with Kotlin) I just don't have to do this anymore. That being said there are two possible sources of nullpointers: from inside your program or from outside (e.g. the json your server got send wasn't fully initialized). The majority of NullPointers come from outside in my experience. Nullpointers from within you have to fix mostly once. but there's no end to what data send to you from outside your control can be missing. Of course you still have to validate/parse outside data when using Kotlin but since the type system then carries on which data has been validated, you can drop the defensive mindset that was appropriate in Java.

So if you have a self-contained, non-safety critical project and want to use/try out an arena-allocated memory approach using Odin seems fine (e.g. looking at you: "raytracing in a weekend").

So yeah, I think there's a niche of projects where the possibility of null pointers isn't a huge deal.

None of my work-related projects fall in that category though :D

One of the rare cases where the answer to a headline question is "yes".
why does the article re-use a lot of talk from Casey's video without quotes? that's very confusing who is talking what
The mistake was not having nullability be expressed in the type system.

At Facebook I used their PHP fork Hack a lot and Hack has a really expressive type system where PHP does not. You can express nullability of a type and it defaults to a type being non-nullable, which is the correct default. The type checker was aware of changes too, so:

    function foo(?A $a): void {
      $a->bar(); // compile error, $a could be null
      if ($a is null) {
        return;
      }
      $a->bar(); // not a compiler error because $a is now A not ?A
      if ($a is ChildOfA) {
        $a->childBar(); // not an error, in this scope $a is ChildOfA
      }
    }
Now Hack like Java used type erasure so you could force a null into something non-nullable if you really wanted to but, in practice, this almost never happened. A far bigger problem was dealing with legacy code that was converted with a tool and returned or used the type "mixed", which could be literally anything.

The real problem with Java in particular is you'd end up chaining calls then get the dreaded NullPointerException and have no idea from the error or the logs what was broken from:

   a.b.c.d();
I'm fine with things like Option/Maybe types but to me they solve different problems. They're a way of expressing that you don't want to specify a value or that a value is missing and that's different to something being null (IMHO).
Exactly, null is not evil it itself – the fact that it's not represented in type system is.

Type system where nullability can be expressed, you have refinement so you can map "null | T" to "T" with conditionals and sugar like optional chaining and nullish coalescing is all that's needed.

Neat.

In PHP land, for some years now that code would not pass CI/CD checks and IDEs show red squiggles. Provided they use any popular static analysis tools like PHPStan, Psalm and I believe SonarQube would also flag it.

Yeah in Typescript I rarely run into null deference errors at runtime either. It can happen if you unsafely cast the type of values coming into your code, but if you runtime validate input at your application boundary it’s very unlikely

Now looking back at a lot of other languages that don’t express nullability, it’s like, what were they thinking? How did I not wish for nullability in type declarations in all my years of dealing with NullPointerExceptions?

> The real problem with Java in particular is you'd end up chaining calls ... and have no idea from the error or the logs what was broken from: a.b.c.d();

That’s been solved since Java 14. (5 years ago) Now the error will tell you exactly what was null.

And “soon” Java will have built in support for expressing nullability in the type system. Though with existing tools like NullAway it’s already (in my opinion) a solved problem.

Are there tradeoffs between individual-element and group-element mindsets with regards to project scale? Does the group-element/global architecture mindset require holding the whole program in my head? Is that even possible for large projects?

I have no experience working in C. Obviously, some of the biggest and most important codebases on earth are C.

Right off the bat, disagree. I always bring up this example but it's actually expanded. On some embedded systems, there is RAM at 0x0. It is writeable and readable. On even more, there's ROM there. Countless times I've had a null pointer dereference be not only a silent failure that wreaks havoc downstream but also a valid operation.

In other times, I've had a malloc fail because I didn't have my heap setup, the code I had pulled in didn't check, and there goes my interrupt vector table or whatever.

When I was making a simulator for one of these chips I explicitly turned off that section of memory do I could catch null pointer dereferences. Not trivial to do in practice

The original title is "Was it really a Billion Dollar Mistake?"

The title should be updated to match the original per the HN guidelines.

I don't get the "individual" vs "grouped" element thing at all. I guess you could interpret everything as a list, where null is actually empty list, but it seems like that would be a ton of boilerplate and architecture astronomy, not a universally better way of programming. I don't see any decent explanation of what "grouped-element" programming is and why it's universally better and more advanced.

If that was really true, you should be able to point to a subset of code developed with this supposedly better mindset and demonstrate that it's actually better in terms of some objective measure like speed, memory efficiency, features, security vulnerabilities, etc. I don't see anything like that though. What are the odds that it really is a better way of programming versus a theory some guy made up? If you're familiar with the "no silver bullets" thing, a ton of things have been promoted by various people in the industry as a universal solution to general problems of software quality at various times, but vanishingly few of them have ever made a real difference.

It seems to me that advances in memory management have been one of the few advancements promoted over the decades to make programming genuinely better and more reliable. Witness the dominance of garbage-collected languages for virtually everything that could possibly be done with them, and the advantages of moving things that can't use GC to languages with more solid non-GC memory management like Rust, which are becoming more clear. I think that's the best element we can lean on for software quality, not this grouped-element stuff.

Here's an example. Imagine you're writing a parser that creates AST tree in C++.

What 99% of people do is the "individual" style where every node is allocate with new and the memory comes from generic OS allocator.

Then when you free the AST tree, you have traverse the tree and call delete on every node.

There can be thousands of nodes.

The crucial observation is: the lifetime of all nodes is the same. It's the lifetime of AST tree.

The "grouped" thing is: you have an allocator dedicated to nodes of the AST tree. All nodes are allocated from this allocator.

This has numerous speed and simplicity benefits.

You need one free() call to free allocator vs. thousands of free() for each node.

You can optimize your allocator for that use case compared to general OS allocator. By definition it's a bump pointer allocator (i.e. allocation is mostly addr += sizeof(obj)) which is much faster than what even fastest general allocators can do.

You don't need to track per-allocation metadata that general allocator needs to be able to individually free each allocation. So you use less memory.

There's no fragmentation because your allocator is contiguous space.

The use of cache lines is most likely better because memory is not spread around. The nodes are used together so it's better if they are close in memory.

Those are very significant benefits and yet, as the article notices, very few people are aware of this.

Plus until very recently (before Rust became popular) the only serious low-level language was C/C++ and they don't help you programming in this style.

Odin, zig, jai do. They make an allocator an exposed thing and provide language and standard library support for using different allocators.

It all starts to unravel badly once we get here:

> In theory, null is still a perfectly valid memory address

Crucially C's pointers are not addresses. So now we're at best talking only about Odin, which wasn't the subject of Tony's "Billion dollar problem" claim in the first place.

And honestly I doubt that in practice Odin's pointers are just addresses either because Odin uses LLVM.

Muratori followers tend to beat around the bush to avoid saying it directly, but the implication here is that anyone who sees value in Rust is simply a bad programmer and will either stay a bad programmer or eventually stop using the language. Same goes for any language with garbage collection.

A quote from the linked video starting around 8:30:

> All of the advice you see online pretty much is for people thinking at this level, which is not where you want to be. You're gonna have to go through it at some point to understand what's going on, but your goal is to go from [individual element thinking] to [grouped element thinking], so advice like 'use smart pointers' or the rust borrow checker, all that stuff? Those are for people who are still not very good at programming.

To be clear: I do not agree with this position, but I thought it was important to clarify what I believe to be the intended message of this article (and others that you see coming from this group of people).

I’m not sure that the distinction between “element-wise” and “group-wise” mindsets are mutually exclusive in the design space for a language.

The set of programs that can express problems as transformations on arrays is quite large.

But I think there’s a large overlap with programs that also need to manage resource lifetimes as well where a resource could be a region of foreign-allocated allocated memory, a database connection, etc; a process-scoped object that can only be acquired (or not) at runtime.

Even in Odin you’re going to need to think in terms of arrays and how individual element lifetimes at some point depending on how you classify and structure data. The trade off seems to be that how you specify and maintain contracts for the lifetime of objects like sockets and handles are up to you and Odin won’t help you with that. In return you get array oriented initialisation?

Or am I misunderstanding the mental model?

His framing of individual-element mindset vs. grouped-element mindset is very clear sighted.

It's what frustrates me so much about Rust - the language puts so much focus and complexity onto something that you shouldn't be doing in the first place: allocating and freeing millions of tiny objects. You end up writing programs that spend half their time allocating, initialising, destroying and freeing elaborate trees of records, and devote half their memory to pointers.