I think the notion that "null" is a billion dollar mistake is well overblown. NULL/nil is just one of many invalid memory addresses, and in practice most of invalid memory address are not NULL. This is related to the drunkard’s search principle (a drunk man looks for his lost keys at night under a lamppost because he can see in that area). I have found that null pointers are usually very easy to find and fix, especially since most platforms reserve the first page of (virtual) memory to check for these errors.
In theory, NULL is still a perfectly valid memory address it is just that we have decided on the convention that NULL is useful for marking a pointer as unset.
Many languages (including Odin) now have support for maybe/option types or nullable types (monads), however I am still not a huge fan of them in practice as I rarely require them in systems-level programming. I know very well this is a "controversial" opinion, but systems-level programming languages deal with memory all the time and can easily get into "unsafe" states on purpose. Restricting this can actually make things like custom allocators very difficult to implement, along with other things.
n.b. Odin's `Maybe(^T)` is identical to `Option<&T>` in Rust in terms of semantics and optimizations.
> I have found that null pointers are usually very easy to find and fix, especially since most platforms reserve the first page of (virtual) memory to check for these errors.
This is true. However, you have done these fixes after noticing them at runtime. This means that you have solved the null problem for a certain control + data state in code but you don't know where else it might crop up again. In millions of lines of code, this quickly becomes a whack-a-mole.
When you use references in Rust, you statically prove that you cannot have null error in a function for all inputs the function might get if you use Rust style references. This static elimination is helpful. Also you force programmers to distinguish between &T and Option<&T> and Result<&T,E> -- all of which are so common in system's code.
Today it is safe to assume that a byte is 8 bits. Similarly it is safe to assume that the first page in virtual memory is non-readable and non-writable -- why not make use of this fore knowledge ?
> This is related to the drunkard’s search principle (a drunk man looks for his lost keys at night under a lamppost because he can see in that area).
This is a nice example and I do agree in spirit. But then I would offer the following argument: Say, a problem (illegal/bad virtual address) is caused 60% by one culprit (NULL dereference) and 40% by a long tail of culprits (wrong virtual memory address/use after free etc). One can be a purist and say "Hey, using Rust style references" only solves the 60% case, addresses can be bad for so many other reasons ! Or one can pragmatic and try to deal with the 60%.
I cringe every time I see *some_struct in Linux kernel/system code as function argument/return. Does NULL indicate something semantically important ? Do we need to check for NULL in code that consumes this pointer ? All these questions arise every time I see a function signature. Theoretically I need to understand the whole program to truly know whether it is redundant/necessary to check for NULL or not. That is why I like what Rust and Zig do.
But to answer your general points here: Odin is a different language with a different way of doing things compared to others, so their "solutions" to specific "problems" do not usually apply to a language like Odin.
The problem with solving the "60% case" means you now have introduced a completely different way of programming, which might solve that, but the expense of so many other cases. It's a hard language design question and people focusing on this specific case have not really considered how it effects anything else. Sadly, language features and constructs are rarely isolated from other things, even the architecture of the code the programmer writes.
As for C code, I agree it's bad that there is no way to know if a pointer uses NULL to indicate something or not, but that's pretty much not a problem in Odin. If people want to explicitly state that, they either use multiple return values, which is much more common in Odin (which is akin to Result<&T, E> in Rust, but of course not the same for numerous reasons), or they use `Maybe(^T)` (akin to Option<&T> in Rust).
I understand the problems that C programmers face, I am one, which is why I've tried to fix a lot of them without trying to be far from the general C "vibe".
> It's a hard language design question and people focusing on this specific case have not really considered how it effects anything else. Sadly, language features and constructs are rarely isolated from other things, even the architecture of the code the programmer writes
And my suggestion is that Rust has got the balance right when it comes to pointers. I can use the traditional unsafe nullable pointer *SomeStruct when it can be null and use &SomeStruct when it cannot be NULL in Rust. Initialization can be a bit painful in Rust but the wins are worth it. Yes, initialization can be less efficient in Rust but then most of the time spent is spend in algorithms when they run, not during initialization of the data structure.
Rust has needless complexity when it comes to asynchronous programming. The complexity is off the charts and the language just becomes unusable. But the non-async subset of Rust feels consistent and well engineered from a programming theory perspective.
In summary, Rust has not compromised itself by using non-null references as the default pointer type and neither will Odin, if it takes a different approach towards references. Take the example of OCaml - it also takes a very principled approach towards NULL. OTOH Java suffers from NULL problems as every object could be null in disguise.
Nevertheless Odin is a remarkably clean and powerful language and I'm following its progress closely ! Thanks for building it !
Unfortunately I don't think you've understand what I was I trying to say.
Rust was designed from day-0 around explicit individual-value based initialization. Odin from day-0 was not designed around this (explicitly).
This ever so minor choice might not seem like a big thing to you, as you have stated in your comment, but it leads to MASSIVE architectural decisions later on when the user programs.
Odin could not "just add" non-nil pointers and it be "fine". It would actually not be Odin any more, and the entire language would not even be a C alternative any more, and feel much more like C++ or Rust. Rust and OCaml (which Rust is based off) are different kinds of languages to Odin and their approach does not translate well to what Odin (or C) is trying to do.
Unfortunately I will not be able to explain this to you in a comment or article, and it is something that takes a while to understand since it is a subtle effect of locality affecting globality. The best video on this might be from Casey Muratori: https://www.youtube.com/watch?v=xt1KNDmOYqA
> Yes, initialization can be less efficient in Rust but then most of the time spent is spend in algorithms when they run, not during initialization of the data structure.
Sadly this isn't as true as you think it is. I've written a lot of C++ code before and doing its boilerplate for ctors/dtors actually leads to much slower code in general, and I'd argue this does apply to Rust too. Most of the time isn't necessarily spent in "algorithms", especially when "algorithms" also include initialization of values. You've turned something which could be O(1) into at best O(N), which does not help when things scale, especially with "algorithms".
see, this seems like something that's nice to actually put into the types; a Ptr<Foo> is a real pointer that all the normal optimizations can be done to, but cannot be null or otherwise invalid, and UnsafePtr makes the compiler keep its distance and allows whatever tricks you want.
Yes it's the burden of proof. That's why writing Rust is harder than C++. Or why Python is easier than anything else. As a user and customer, I'd rather pay more for reliable software though.
n.b. Sorry for the long reply, this is actually a really complex and complicated topic in terms of language design, trade-offs, and empirics.
It's a trade-off in design which is not immediately obvious. 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 (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.
The first option is solved with `Maybe(^T)` in Odin, and that's fine. It's actually rare it is needed in practice, and when it is needed, it's either for documenting foreign code's usage of pointers (i.e. non-Odin code), or it's for things which are not pointers at all (in Odin code).
The second option is a subtle one: it forces a specific style and architectural practices whilst programming. Odin is designed around two things things: to be a C alternative which still feels like C whilst programming and "try to make the zero value useful", and as such, a lot of the constructs in the language and core library which are structured around this. Odin is trying to be a C alternative, and as such it is not trying to change how most C programmers actually program in the first place. This is why you are allowed to declare variables without an explicit initializer, but the difference to that of C is that variables will be zero-initialized by default (you can do `x: T = ---` to make it uninitialized stack memory, so it is an opt-in approach).
Fundamentally this idea of explicit individual-value based initialization everywhere is a viral concept which does lead to what I think are bad architectural decisions in the long run. Compilers are dumb---they cannot do everything for you, especially know the cost of the architectural decisions throughout your code, which requires knowing the intent of your code. When people argue that a lot of the explicit initialization can be "optimized" out, this is only thinking form a local position of individual values, which does total up to being slower in some cases.
To give an example of what I mean, take `make([]Some_Struct, N)`, in Odin, it just zeroes the memory because in some cases, it is literally free (i.e. `mmap` must zero). However, when you need to initialize each value of that slice, you are not turning a O(1) problem into a O(N) problem. And it can get worse if each field in the struct also needs its own form of construction.
But as I said in my original comment, I do not think the `nil` pointer problem, especially in Odin since it has other array constructs, is actually an empirical problem in practice. I know a lot of people want to "prove" things whatever they can at compile-time, but I still have to test a lot of code in the first place, and for this very very specific example, it is a trivial one to catch.
P.S. This "point" has been brought up a lot before, and I do think I need to write an article on the topic explaining my position because it is a bit tiring rewriting the same points out each time.
P.P.S. I also find this "gotcha" people bring up is the most common one because it is _seems_ like an obvious "simple" win, and I'd argue it's the exact opposite of either "simple" and even a "win". Language designing is all about trade-offs and compromises as there is never going to be a perfect language for anyone's problem. Even if you designed the DSL for your task, you'll still have loads of issues, especially with specific semantics (not just syntax).
In theory, NULL is still a perfectly valid memory address it is just that we have decided on the convention that NULL is useful for marking a pointer as unset.
Many languages (including Odin) now have support for maybe/option types or nullable types (monads), however I am still not a huge fan of them in practice as I rarely require them in systems-level programming. I know very well this is a "controversial" opinion, but systems-level programming languages deal with memory all the time and can easily get into "unsafe" states on purpose. Restricting this can actually make things like custom allocators very difficult to implement, along with other things.
n.b. Odin's `Maybe(^T)` is identical to `Option<&T>` in Rust in terms of semantics and optimizations.