> 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 [...] explicit initialization of every value everywhere.
I used to be an "initialize everything" partisan, but meaningful zero-values have grown on me. I still don't think everything should be zero-initialized, though (that is, valid when initialized to zero). I'd prefer it if there were a two-colour system where a type is only zero-initialized if it don't contain a pointer, either directly or transiently.
The trick would be that zero-initialized sum types have a default variant, and only that variant needs to be zero-initialized. So a type that requires explicit initialization can be made zero-initialized by wrapping it with Optional<T>, whose default value is the zero-initialized None value. So even though you end up with coloured data types, they're easily contained & they do not spread virally.
I think this offers the best of both worlds. It gives you explicit nullability while still permitting something like `make([]SomeStruct, n)` to return a big block of zeroes.
> Languages like Rust were designed from day zero around explicit individual-element based initialization
> Ownership is a constant concern and mental overhead when thinking in an individual-element mindset. Whilst ownership is obvious (and usually trivial) in the vast majority (99+%) of cases when you are in the grouped-element mindset.
I think there's a sort of convergence here, because one of the tips that's often recommended for dealing with lifetimes issues in Rust (e.g. cyclic data structures) is grouping elements together in a parent structure and using handles instead of references. Often this is done with integer handles in a simple Vec<T>, but libraries like slotmap¹ exist which make that pattern safer & more ergonomic, if desired.
Ownership and lifetimes are a source of essential complexity in programming. I don't see any contradiction between managing them with a grouped-element mindset and managing them with language features; in fact, I think the two go hand-in-hand quite nicely.
I mostly agree. Null pointer de-references aren't a dominant or hard-to-detect failure mode for my code either. Removing them does meaningfully complicate language design and ergonomics too. It's reasonable to call them a "problem" in the same sense that any invalid memory address is a problem. I also see value in making invalid states unrepresentable which would mean disallowing null pointers right? The real question is whether that gurantee is worth the cost in ergonomics, compiler surface area, and C compatibility. Every invariant a compiler enforces consumes complexity budget. I agree with the author that it's better to keep the core language small and spend that budget on higher-impact guarantees. And the thesis here seems to be that null pointers aren't high-impact enough (and at least for the code I've read and wrote, this seems to hold true). I suspect that part of the appeal is that null pointers are low-hanging fruit. They're easy to point out and relatively easy to "solve" in the type system, which can make the win feel larger than it actually is. The marginal benefit feels smaller than spending that complexity budget on harder, higher-impact guarantees that target the logical traps programmers fall into with clever, highly abstracted code, where bugs are more subtle and systemic rather than immediate and loud.
> I suspect that part of the appeal is that null pointers are low-hanging fruit. They're easy to point out and relatively easy to "solve" in the type system, which can make the win feel larger than it actually is.
I agree. I find that Options are more desirable for API-design than making function-bodies easier to understand/maintain. I'm kind of surprised I don't use Maybe(T) more frequently in Odin for that reason. Perhaps it's something to do with my code-scale or design goals (I'm at around 20k lines of source,) but I'm finding multiple returns are just as good, if not better, than Maybe(T) in Odin... it's also a nice bonus to easily use or_return, or_break, or_continue etc. though at this point I wouldn't be surprised if Maybe(T) were compatible with those constructs. I haven't tried it.
To add, Go has nil pointers which lead to panics when de-referenced. No one would call Go memory unsafe for this behavior likely because it's a "panic". The thing is, the 0 address is a guaranteed panic too. It's just the OS is going to print "Segmentation fault" instead of "panic". There's nothing memory unsafe about this. The process will with 100% gurantee, reliably, and deterministically exit in the same way as a "panic" in any other language here. The unsafe aspect is when it's not a nil pointer but a dangling pointer to something that may still exist and read garbage. That is unsafe.
My feelings have evolved so much on Option types... When Swift came around I'm sure I would've opposed the information in this article. For two reasons.
1. I was a mobile dev, and I operated at the framework-level with UIKit and later SwiftUI. So much of my team's code really was book-keeping pointers (references) into other systems.
2. I was splitting my time with some tech-stacks I had less confidence in, and they happened to omit Option types.
Since then I've worked with Dart (before and after null safety,) C, C++, Rust, Go, Typescript, Python (with and without type hints,) and Odin. I have a hard time not seeing all of this as preference, but one where you really can't mix them to great effect. Swift was my introduction to Options, and there's so much support in the language syntax to help combat the very real added-friction, but that syntax-support can become a sort of friction as well. To see `!` at the end of an expression (or `try!`) is a bit distressing, even when you know today the unlikelihood (or impossibility) of that expression yielding `nil.`
I have come to really appreciate systems without this stuff. When I'm writing my types in Odin (and others which "lack" Optionals) I focus on the data. When I'm writing types in languages which borrow more from ML, I see types in a few ways; as containers with valid/invalid states, inseparably paired with initializers that operate on their machinery together. My mental model for a more featureful type-system takes more energy to produce working code. That can be a fine thing, but right now I'm enjoying the low-friction path which Odin presents, where the data is dumb and I get right to writing procedures.
> *TL;DR* 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. The trivial solutions to remove the “problem” null pointers have numerous trade-offs which are not obvious, and the cause of why people think it is a “problem” comes from a specific kind of individual-element mindset.
> 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 [...] explicit initialization of every value everywhere.
I used to be an "initialize everything" partisan, but meaningful zero-values have grown on me. I still don't think everything should be zero-initialized, though (that is, valid when initialized to zero). I'd prefer it if there were a two-colour system where a type is only zero-initialized if it don't contain a pointer, either directly or transiently.
The trick would be that zero-initialized sum types have a default variant, and only that variant needs to be zero-initialized. So a type that requires explicit initialization can be made zero-initialized by wrapping it with Optional<T>, whose default value is the zero-initialized None value. So even though you end up with coloured data types, they're easily contained & they do not spread virally.
I think this offers the best of both worlds. It gives you explicit nullability while still permitting something like `make([]SomeStruct, n)` to return a big block of zeroes.
> Languages like Rust were designed from day zero around explicit individual-element based initialization
> Ownership is a constant concern and mental overhead when thinking in an individual-element mindset. Whilst ownership is obvious (and usually trivial) in the vast majority (99+%) of cases when you are in the grouped-element mindset.
I think there's a sort of convergence here, because one of the tips that's often recommended for dealing with lifetimes issues in Rust (e.g. cyclic data structures) is grouping elements together in a parent structure and using handles instead of references. Often this is done with integer handles in a simple Vec<T>, but libraries like slotmap¹ exist which make that pattern safer & more ergonomic, if desired.
Ownership and lifetimes are a source of essential complexity in programming. I don't see any contradiction between managing them with a grouped-element mindset and managing them with language features; in fact, I think the two go hand-in-hand quite nicely.
[1]: https://docs.rs/slotmap/latest/slotmap/index.html
I mostly agree. Null pointer de-references aren't a dominant or hard-to-detect failure mode for my code either. Removing them does meaningfully complicate language design and ergonomics too. It's reasonable to call them a "problem" in the same sense that any invalid memory address is a problem. I also see value in making invalid states unrepresentable which would mean disallowing null pointers right? The real question is whether that gurantee is worth the cost in ergonomics, compiler surface area, and C compatibility. Every invariant a compiler enforces consumes complexity budget. I agree with the author that it's better to keep the core language small and spend that budget on higher-impact guarantees. And the thesis here seems to be that null pointers aren't high-impact enough (and at least for the code I've read and wrote, this seems to hold true). I suspect that part of the appeal is that null pointers are low-hanging fruit. They're easy to point out and relatively easy to "solve" in the type system, which can make the win feel larger than it actually is. The marginal benefit feels smaller than spending that complexity budget on harder, higher-impact guarantees that target the logical traps programmers fall into with clever, highly abstracted code, where bugs are more subtle and systemic rather than immediate and loud.
> I suspect that part of the appeal is that null pointers are low-hanging fruit. They're easy to point out and relatively easy to "solve" in the type system, which can make the win feel larger than it actually is.
I agree. I find that Options are more desirable for API-design than making function-bodies easier to understand/maintain. I'm kind of surprised I don't use Maybe(T) more frequently in Odin for that reason. Perhaps it's something to do with my code-scale or design goals (I'm at around 20k lines of source,) but I'm finding multiple returns are just as good, if not better, than Maybe(T) in Odin... it's also a nice bonus to easily use or_return, or_break, or_continue etc. though at this point I wouldn't be surprised if Maybe(T) were compatible with those constructs. I haven't tried it.
To add, Go has nil pointers which lead to panics when de-referenced. No one would call Go memory unsafe for this behavior likely because it's a "panic". The thing is, the 0 address is a guaranteed panic too. It's just the OS is going to print "Segmentation fault" instead of "panic". There's nothing memory unsafe about this. The process will with 100% gurantee, reliably, and deterministically exit in the same way as a "panic" in any other language here. The unsafe aspect is when it's not a nil pointer but a dangling pointer to something that may still exist and read garbage. That is unsafe.
My feelings have evolved so much on Option types... When Swift came around I'm sure I would've opposed the information in this article. For two reasons.
1. I was a mobile dev, and I operated at the framework-level with UIKit and later SwiftUI. So much of my team's code really was book-keeping pointers (references) into other systems.
2. I was splitting my time with some tech-stacks I had less confidence in, and they happened to omit Option types.
Since then I've worked with Dart (before and after null safety,) C, C++, Rust, Go, Typescript, Python (with and without type hints,) and Odin. I have a hard time not seeing all of this as preference, but one where you really can't mix them to great effect. Swift was my introduction to Options, and there's so much support in the language syntax to help combat the very real added-friction, but that syntax-support can become a sort of friction as well. To see `!` at the end of an expression (or `try!`) is a bit distressing, even when you know today the unlikelihood (or impossibility) of that expression yielding `nil.`
I have come to really appreciate systems without this stuff. When I'm writing my types in Odin (and others which "lack" Optionals) I focus on the data. When I'm writing types in languages which borrow more from ML, I see types in a few ways; as containers with valid/invalid states, inseparably paired with initializers that operate on their machinery together. My mental model for a more featureful type-system takes more energy to produce working code. That can be a fine thing, but right now I'm enjoying the low-friction path which Odin presents, where the data is dumb and I get right to writing procedures.
The first paragraph from the article:
> *TL;DR* 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. The trivial solutions to remove the “problem” null pointers have numerous trade-offs which are not obvious, and the cause of why people think it is a “problem” comes from a specific kind of individual-element mindset.