I think that's the bigger issue in Java. `int` can't be used in generics, so now you use the boxed type `Integer` which needs to be checked on each use, which depending on the usage is a lot of work.
If Java had proper generics without this workaround, we wouldn't have nearly as many issues.
In an alternative universe, there could be n sentinel non-values, instead of a single null.
There could be "null" ("I am initialising this memory as uninitialised"), but there could also be "egad" ("this pointer refers to memory on a different page"), there could be "biff" ("this address is on another caller's stack").
There are infinite ways you could design a language which lets you take invalid memory and tell the type system it's a Sheep, when it's not. Some of those languages might even have sophisticated biff-checkers or raise EgadReferenceExceptions.
What's stopping you from throwing out null along with biff and egad?
Obviously we should be able to represent objects with possibly invalid state, but rather than allowing a "possibly invalid value" as a legitimate member of a huge number of types, which is what nil/null permits, the possible invalidity should be it's own type, like a MaybeInvalid<T>. Even languages that bake null checking into the type system are doing this, and then just inserting implicit conversions to/from such a type.
Safe Rust doesn't have any either, though presumably you'll say "but unsafe" there.
> It is generally desirable to keep invalid objects properly typed
As written this doesn't mean anything, if the object is properly typed then it is valid, an invalid object can't be properly typed. So more likely you're imagining something like Rust's MaybeUninit<T> type wrapper which says well, either this is a T, or it isn't (yet) and so a programmer will need to tell the compiler once they've arranged that it is a T so then it can be used as a T.
Barry Revzin has (I think) landed the fixes to the C++ type system so that you could reasonably write this in C++ too once you have C++ 26.
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.
- 1: In C (and relatives), you cannot rule out that any pointer is not-null. Simply due to pointer arithmetic.
- 2: Some values have no default-zero state.
On #2 I found the EG discussions on the Java mailing lists to be fascinating. Think of innocuous types such as "date", where a default value only causes problems. You end up with something like "1970:0:0" or "0:0:0" and it acts like a valid date, but its use is likely unintentional and leads to follow-up issues.
And once you have multi-threading involved, even a simple null-flag becomes a difficult technical challenge to implement under all conditions.
I've always understood the billion dollar mistake to be more about #2 and language like Java in particular. Agree about default values being bad, it's one of my primary reservations with Go.
Fair point. Still, it just leaves a bitter taste when you want to express something as non-null but can't technically exclude it...
- 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.
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.
E.g.,
std::size_t std::strlen(const char* str);
Object value = a.b.c.d;
for something like a JSON value you got over the web where any of it might be null but have to write something like Object value = nonNull(a) ? nonNull(a.b) ? nonNull(a.b.c) ? d : null : null : null
or Object value = null
try {
value = a.b.c.d
} catch(NullPointerException x) {}
Just being able to write something like const value = a?.b?.c?.d;
is a great relief. If it is just value lookup letting a.b return null if a is null would be fine with me but there is something a little creepy about a.doSomething() doing nothing and returning null in that case.Personally I am not a fan of Optional<T> and (worse) Either<A,B> in Java for various reasons. I think the official functional stuff in Java (like the streams API) is awful, but I like working with functions like
Object value = nullSafe(a,x=>x.b,x=>x.c,x=>x.d)
The author has some affinity towards a collection-first or collection-only style of programming and certainly I have written programs or subprograms working with dynamic structures that leaned heavily into List<T>, that is, a value which might be present or absent can be treated as a List which just happens to have 0..1 members and x.map(y=>...) and other functional operators do the right thing for the 0..1 and 0..N cases and if you use the same set of operators for both of those cases they just roll off your fingers and you are less likely to forget to put in null checks and when you compose more complex operators out of simpler ones it tends to "just work"Putting on my theory hat, while we we can overload field projection for any monad (data structure representing a result + an effect : think result for exceptions, or a thunk for a promise), it's not the best idea.
But for what is at least morally a commutative (order doesn't matter: FAIL ; pure = pure ; FAIL = FAIL) and idempotent (FAIL ; FAIL = FAIL) monad, it works...
Which justifies fun things, like lazy projections on promises!
((A) null).someValue
or ((A) null).doSomething()
does at the field or method level. I guess there is the issue of how you figure out what the type is which depends of course on the language. Could be a better fit for C++ than all-virtual Java but I don't want to give the C++ any ideas to make that language more complex!Versus if I instead view nullability as a way of transforming types (aka a functor) that works by reflection; this gives me some parametricity: the behaviour of my function will be the "same" regardless of which generic I slot in (though fields don't play that well with generics... something something row polymorphism something something).
Ill formed thoughts really; what I'm handwaving at is I slightly agree with the anti-complexity crowd that an operator should usually do the same thing, and yet I think it harms terseness and hence often readability and understanding to have non-overloaded boilerplate everywhere (addf64, addu32, addi16...).
Parametricity is a nice compromise for non-primitive ops, imo.
(1) As a simple set of primitive operations that I can use to build up increasingly complex operations and on that level I value "simple". Like the all-virtual method dispatch in Java as compared to whatever it is in C# or C++. In that case I value predictability and users knowing what they're going to get.
(2) As a platform for domain-specific languages such as the tactics described in On Lisp although those tactics apply well to other languages. In that case my desires as an "author of libraries" and "users of libraries" start to diverge. With that hat on I start to think (a) application programs can consist of "islands" with radically different coding styles (say in Java part of the system is comfortable with POJOs but other parts work with dynamically structured data with no schema or a schema defined at runtime) and (b) I'd like to be able to bend the language really far.
From the viewpoint of a programmer who wants to get work and have an impact on the industry I'm usually working in commercially mature languages and finding as much of those values in them as I can.
So yes, I wish there was a commonly used way to express which of these properties of a pointer will be exploited by the function.
Optional is `Option<T>`
Zero-copy is `&T`
Mutation is `&mut T`
Diehard C programmers have Stockholm Syndrome for the language because they like to show off how they can be productive in a bad language. If they took a few months to learn C++ / Rust / C# / any language that has solved this, they'd have to admit that they staked a lot of their ego on a language that constantly makes them jump through hoops. Because they love showing that they're good hoop-jumpers.
But any noobie who's a year into programming will say "Oh cool, in those languages I don't have null pointer exceptions" and never learn C. Good!
[1] One big issue is that GCC takes this hint and uses it in the optimizer, so you cannot have both a compile time and a run time check for a non-NULL parameter.
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-...
The remaining challenge is interfacing with unsafe code. Unsafe code should be required to check all pointers for null before allowing it back into the safe parts of the codebase.
C# 8 added "nullable reference types". For example string? is a type which can be any string, including the empty string "", or it can be null. What's the name of my oldest report? "Steve" ? "Mary" ? No, I do not have anybody reporting to me, it's null. Historically all reference types were nullable, it wasn't optional. In C# 1.0 any string could be null, whereas in C# 8 you could explicitly say that some strings mustn't be null.
However almost all C# software runs on the CLR, and the CLR doesn't believe in these rules, so where your C# is called by other software, possibly not even written in C# you do need to know that all your function boundaries don't actually enforce null checks. This matters most if you write libraries used by third parties in other programming languages, and least for in-house or personal stuff where it clearly goes in the "Don't do that" pile.
Perhaps think of a C# 8.0+ string parameter as a stern "Do not use null strings" sign rather than a firm language commitment like Rust's &str or a C++ &std::string. That new intern will obey it, that nerd who writes machine code for fun will at least know it's their fault if they don't obey it, but the customer on another continent who has apparently never read any of your documentation can't be relied upon to uphold this constraint.
[Edited: fixed numerous typos, there are doubtless more]
> 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.
In Kotlin (and Rust, Swift, ...) these are not the only options. You can check a pointer/reference once, and then use it as a non-nullable type afterwards. And if you don't want to do that, you can just add !!/!/unwrap: you are just forced to explicitly acknowledge that you might blow up the entire app.
> 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 think Hoare’s own quote has exactly that nuance in it: reliability and predictability are given as much prominence as security.
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.
> 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?
He worked with Jonathan Blow on "The Witness"[0].
As the developer of the "Bink 2" video codec[1] and the animation tool "Granny 3d"[2], his code powers thousands of games.
[0] https://store.steampowered.com/app/210970/The_Witness/, [1] https://www.radgametools.com/bnkmain.htm, [2] https://www.radgametools.com/granny.html
Some of those games (though not all of them, unfortunately) try to work around C#'s garbage collector for performance reasons using essentially adhoc memory allocators via object pools and similar approaches. This is probably what this part...
--- And if you ever do think about these, it’s usually because you are trying to do something performance-oriented and you have to pretend you are managing your own memory. It is common for many games that have been written with garbage collected languages to try to get around the inadequacies of not being able to manage your own memory ---
...is referring to.
> Casey, meanwhile, has worked on a low-level game development video series for a decade and... never actually shipped a game?
These videos are all around 90-120 minutes long, each posted with gaps between them since the previous (my guess is whenever Casey had time) and the purpose and content of these videos is pedagogical so he spends time explaining what he does - they aren't just screencasts of someone writing code.
If you combine the videos and assuming someone works on it 6h/day with workdays alone it'd take around 8-9 months to write whatever is written there but this also ignores the amount of time spent on explanations (which is the main focus of the videos).
So it is very misleading to use the series as some sort of measure for what it'd take Casey (or anyone else, really) to make a game using "low level" development.
Ahh, I was just thinking about this morning.
Remember the Muratori v Uncle Bob debate? Back then the ad-hominems were flying left and right, with Muratori being the crowd favourite (a real programmer) compared to Uncle Bob (who allegedly didn't write software).
Then a few months ago Muratori gave a really interesting multi-hour talk on the history of OOP (including plenty of well-thought out criticism). I liked the talk, so I fully expected a bunch of "real programmers" to shoot that talk down as academic nonsense.
Anyway, looks like Muratori is right on schedule to graduate from programmer to non-programmer.
The OP implies heavily that writing a program in a language with anything but pure manual memory management makes you lesser as a programmer than him: "Unfortunately, a lot of people never move past this point on their journey as a programmer" implies he has moved further on in his "journey" than those that dare to use a language with GC.
(and with respect to C++ note that OP considers RAII to be deficient in the same way as GC and ARC)
Its not near the top of the list of reasons std::unordered_map is a crap type, but it's certainly on there. If we choose the capacity explicitly knowing we'll want to put no more than 8340 (key,value) pairs into Rust's HashMap we only allocate once, to make enough space for all 8340 pairs because duh, that's what capacity means. But std::unordered_map doesn't take the hint, it merely makes its internal hash table big enough, and each of the 8340 pairs is allocated separately.
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.
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.
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
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).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.
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.
I have no experience working in C. Obviously, some of the biggest and most important codebases on earth are C.
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 title should be updated to match the original per the HN guidelines.
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.
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.
I have actually heard of something like that, as arena allocation, though mostly as something to use in a GCed language to either avoid or reduce the impact of GC cycles, also for certain specific scenarios.
> 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.
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).
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?
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.
Kinrany•21h ago
That doesn't sound right: the third option is making the compiler check that only initialized memory is read.
rzwitserloot•21h ago
mcny•21h ago
Let me explain.
Web browsers have the option to reject syntax errors in HTML and immediately fail, but they don't. Similarly, I think compilers could be a bit slower, check a few more things, and refuse to produce an output if a pointer could be null.
I'm just a code monkey, so I don't have all the answers, but is it possible to have a rule like "when in doubt, throw an error"? The code might be legal, but we aren't here to play code golf. Why not deny such "clever" code with a vengeance and let us mortals live our lives? Can compilers do that by default?
nixosbestos•21h ago
adastra22•20h ago
delaminator•20h ago
Netscape Navigator did, in fact, reject invalid HTML. Then along came Internet Explorer and chose “render invalid HTML dwim” as a strategy. People, my young naive self included, moaned about NN being too strict.
NN eventually switched to the tag soup approach.
XHTML 1.0 arrived in 2000, attempting to reform HTML by recasting it as an XML application. The idea was to impose XML’s strict parsing rules: well-formed documents only, close all your tags, lowercase element names, quote all attributes, and if the document is malformed, the parser must stop and display an error rather than guess. XHTML was abandoned in 2009.
When HTML5 was being drafted in 2004-onwards, the WHATWG actually had to formally specify how browsers should handle malformed markup, essentially codifying IE’s error-recovery heuristics as the standard.
So your proposal has been attempted multiple times and been rejected by the market (free or otherwise that’s a different debate!).
DarkNova6•20h ago
1: C uses pointer arithmetic and any pointer can end up being nil or simply show to the wrong address.
2: Just because memory is initialized, that doesn't mean a correct value is read. Some types don't have a valid zero-state (think of Rational-Numbers or Date). Which is why you need a way to express optionality and want to throw an exception if it the empty-state is accessed.