Because everybody is working on the limit of complexity they can comprehend, so there is no headspace left for dealing with bad ergonomics. We need all the help we can get.
"The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt. – Rob Pike"
When did computer science become about hand holding? has it always been this way? Look at react. It was designed to force functional programming concepts in an OOP manner. Is the future of programming the implementation of tightly controlled interfaces with extreme type safety? I would argue thats where we are going. Things are becoming less expressive, not more.
It's the Java approach. Don't give people footguns and force them to write software in a readable, testable, maintainable style. It works extremely well in software engineering, because you want systems that work reliably and that can be maintained/extended by any other engineer at your org. In the professional software engineering world, "clever" programmers are almost always a horrible drag on their team.
It's not the Java approach at all. The first languages to remove ubiquitous nulls (e.g. MLs) were looking to increase expressivity and modeling abilities, not to force bondage and discipline upon developers.
>When did computer science become about hand holding?
When people with pragmatic goals want to get large teams of new programmers productive fast, and can't expect everyone to be able to fend on their own or can afford the cost of accumulated mistakes.
>Look at react. It was designed to force functional programming concepts in an OOP manner.
Whatever that means, as React has little to do with "OOP manner".
And it is directly convertible to a bool. The primary effectiveness seems to be with operator overloading where a NULL could trigger an integer method instead of a pointer method.
To the small extent to which "since C++11" is even relevant to "from a C/C++ perspective", even in C++11 NULL is still allowed to simply be defined as 0.
This is not even true in C. 0 is a "null pointer literal" when used in pointer context, this does not imply that the actual null pointer has a value of zero.
Ah yes you are right, an interesting point however,
6.3.2.3 Pointers
3. An integer constant expression with the value 0, or such an expression cast to type void *,
is called a null pointer constant. If a null pointer constant is converted to a pointer type,
the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer
to any object or function.
4. Conversion of a null pointer to another pointer type yields a null pointer of that type.
Any two null pointers shall compare equal.
Looking at the definition of NULL:
7.17 Common definitions <stddef.h>
3 The macros are
NULL
which expands to an implementation-defined null pointer constant;
Meaning that NULL is implementation defined, but is assured by the standard to compare equal to any 0-valued pointer of any type, even if it is itself another special value.
Saying “equal to any 0-valued pointer” there can be misleading. It is true of a pointer assigned from a (foo*)0 constant, but not true of a pointer to a hardware address 0 or a pointer with bits all 0 (assuming they exist).
In C, the literal "0" is a null pointer constant handled at the compilation stage, but casting a runtime zero is not specified to yield a null pointer (and the address zero can be perfectly valid and usable), nor are null pointers specified to be zero-valued (quite the opposite).
1. You're comparing a null pointer literal with a null pointer constant. You're literally comparing two things guaranteed to be equal. How, exactly, is it surprising that they do? Did you even attempt to understand what I wrote?
2. The specific implementation you have on hand could use zero-valued null pointers, I'm telling you what the standard doesn't say, confirmed by the C FAQ:
3. The null pointer is not "directly convertible to a boolean 0", it's the literal 0 which expresses a null pointer: http://c-faq.com/null/ptrtest.html
I see what you are saying. I mean zero as null constant. I've never even seen anyone try to assign a null value with some integer zero at some memory address. Using the NULL constant in C/C++ is near effortless and 0 just works no matter what the underlying implementation is doing. So for all intents and purposes nullptr is just a casted NULL (0) constant.
At least C# has the syntactic sugar to easily check for null references which lets you avoid the horrors of code like `(if s != null && s.length)`. Instead you can type `s?.length`. Never have I appreciated syntactic sugar as much.
> This is exactly the kind of minutiae that GP was bemoaning.
How is this “minutiae”? You should always know the possible range of a numeric variable or field when you create it, so why not just write what size it is? In Rust the main numeric types look like this: i32, u64, u8. You just pick the one you want.
A float in Python is an implementation-specific size, just like C. So I'm really confused about using it as an example here.
> Storage classes are "minutiae" however when all you want is just a straight up number.
You can have a single "straight up number" and mention the bit width in the language spec. The mere act of writing it down doesn't force coders to deal with any more minutiae than they already had to deal with.
> Yeah by way of higher-level abstractions ...
I strongly object to this. "float is at least x bits" and "float is exactly x bits" are the same level of abstraction, and almost every language, high or low level, picks one of those options.
You could apply that same "who cares?" attitude to the size of "double" in C. Whether you burden yourself with that knowledge is not a feature of the language. More "C coders" care because they're micro-optimizing, but it's no more needed in C than Python.
Also you named Java as being on the easy side and that has four different integer sizes...
Null-terminated strings are useful for buffering values of unknown length (e.g. copying from a network stream). Length-prefixed strings have issues of their own (e.g. what size length prefix to use, efficient encoding of variable-length integer prefixes, what happens if a length-prefixed string's calculated end-position is outside the process' memory space?)
> Null-terminated strings are useful for buffering values of unknown length (e.g. copying from a network stream).
That doesn't seem like a good example. The data arriving over the network arrives length-prefixed, because 0 is a legal byte value for arbitrary data. What do you then gain by throwing away your existing knowledge of the length?
I came here to write that ASCIIZ is the more costly design decision than null, that not only led to crashing programs, but also to security vulnerabilities, sloppy APIs that cannot handle "binary data" and subpar performance.
I wonder whether the author also hates the 0 and 1 elements of natural numbers. Since they have the same flaw of having weird, special semantics that all other other numbers don't share. In fact 0 is not even a number, but a placeholder for the concept of the absence of a number. Just like NULL.
Could I ask that you update Wikipedia with your discovery? Sadly the page appears to be erroneously using the word "unit" all over the place! https://en.wikipedia.org/wiki/Celsius
It's already there, if you know enough to recognize it:
> The degree Celsius (°C) can refer to a specific temperature on the Celsius scale or a unit to indicate a difference between two temperatures
In other words, you can add a quantity of °C to a temperature value and you'll get another temperature value. But you can't measure a temperature in °C.
Compare how, for example, the python datetime library uses a datetime type and a timedelta type. A datetime plus a timedelta is a datetime. datetimes refer to points in time, and timedeltas don't.
°C measures a temperature delta, but not a temperature.
> It's already there, if you know enough to recognize it
"As an SI derived unit", "or a unit to indicate", "the unit was called"
Three mentions of it being a unit in the first paragraph, alone. I understand the point being made; the conclusion that "celsius is not a unit" is bogus, however, by any common definition, including NIST's.
In a now-deleted comment, you linked to a Wikipedia page on Dimensional Analysis, which includes the sentence "to convert from units of Fahrenheit to units of Celsius".
Except when you take into account that 0 Kelvin actually is theoretically "nothing" with regards to temperature. It's not a floating point, it's an absolute.
I imagine because the post is not pedantic but is just wrong. If you want to be pedantic and correct, the symbol 0 represents a real number, an integer, and other numbers. The symbol 0 is not a cardinal (counting) number. Some people consider the natural numbers to be either the cardinal numbers or the whole numbers which include 0. So some people consider 0 to not be a natural number.
As a mathematical entity it doesn't represent the absence of anything. It is just a symbol that has certain properties associated with it. There isn't a hole in the real number line where 0 should be: there is a number there.
It isn't pedantic to insist that 0 isn't a number, it is equivocal to do so. In most contexts it does not need to be treated like a special value. Temperature measured in degrees is an example where you don't need to treat 0 specially, at least not more than other values...
That's exactly the point the author isn't making - which is that there should be an explicit distinction between Exists But Holds No Value and Does Not Exist.
0 is neither. It's clearly a number which can be used in arithmetic and defines a specific integer quantity. (Unless you think 0 is identical to NaN...)
The problem is that in some languages it's used for either or both of the above logical definitions, when it shouldn't be used for either.
Zero's behavior is totally consistent with the other numbers, though - it doesn't break associativity, commutativity, or any of the other stuff you'd expect. On the other hand, NULL takes every type I've ever written and adds an instance whose behavior with every function is, at best, to crash my program, and at worst, completely undefined. Its behavior is not at all consistent with the other instances.
What the comment about ASCII means is that NUL is a valid character in an ASCII string, but it can't be represented in C's null-terminated string encoding as the format (sometimes called ASCIIZ, but yeah: not an encoding... but I mean, come on... the article is clear here) terminates at the first NUL.
Also, Nil is absolutely a thing in Objective-C: it is a null pointer of type Class (whereas nil is a null pointer of type id; you should avoid mixing them up, though I will admit nothing much bad will happen as Class and id are generally co-polymorphic due to the type system being kind of lame. I am not sure they always have to be, though).
(And as someone who has been programming in C++ since before it was standardized at all, I frankly think listing NULL and boost::optional is totally acceptable and complaining about it as if C++11 is more canonical is just being annoying.)
Doing a quick search for how nil works in Swift, it apparently isn't a null pointer, so you are wrong there as well :(.
> nil means "no value" but is completely distinct in every other sense from Objective-C's nil.
> It is assignable only to optional variables. It works with both literals and structs (i.e. it works with stack-based items, not just heap-based items).
> Non-optional variables cannot be assigned nil even if they're classes (i.e. they live on the heap).
> So it's explicitly not a NULL pointer and not similar to one. It shares the name because it is intended to be used for the same semantic reason.
Given that I don't think any of the rest of your comment was legitimate criticism, I am frankly betting that your comment about UnsafePointer is also not useful, but I am kind of tired of having to analyze this comment at this point (I stepped in due to the note about character sets and the floor kept sinking).
> What the comment about ASCII means is that NUL is a valid character in an ASCII string, but it can't be represented in C's null-terminated string encoding as the format (sometimes called ASCIIZ, but yeah: not an encoding... but I mean, come on... the article is clear here) terminates at the first NUL.
I think ASCIIZ is the more common format, so I replied with all that in response to ASCIIZ being called "unusual". Most popular languages that actually allow NUL bytes in strings usually tend to support some encoding of Unicode anyways…
> Also, Nil is absolutely a thing in Objective-C: it is a null pointer of type Class
You got me…in my defense, I didn't know about this to my knowledge. It was still stupid of me to assume that DuckDuckGo would be case-sensitive when I searched that. I guess I should use this for Classes now instead of nil.
> I frankly think listing NULL and boost::optional is totally acceptable and complaining about it as if C++11 is more canonical is just being annoying.
One ships with C++, one doesn't; that's like saying Joda-Time is the canonical date library for Java instead of java.time. Although, I should probably ask you if you consider Joda-Time to be "more canonical" before listing it as an example…
> Doing a quick search for how nil works in Swift, it apparently isn't a null pointer, so you are wrong there as well :(.
> I am frankly betting that your comment about UnsafePointer is also not useful
I should have been more explicit, since these kind of run together when you bring pointers into the mix, which I guess I should have realized once I read the footnote. I'm not really satisfied with the explanation given in the article, nor your rebuttal of my argument. Swift's nil is overloaded in a sense: for native Swift structures, it's the whole "Optional-as-an-enum" abstraction that we know about. For class types, and pointers, it's a bit more complicated: you just cannot assign nil to a UnsafePointer or a SomeClass unless it's "Optional", but the "Optional-ness" is completely in the type system and under the hood, in order to facilitate interoperatability with C, Objective-C et al. you need to actually have the type of the size be sizeof(void *), have zeroes in it, etc. You cannot actually set either of these types to nil if they are non-Optional unless you do illegal things. So when you set a "pointer" (being an Optional<SomeClass>, UnsafePointer?) here to nil, you are literally shoving a nil it in, which also happens to work well with Swift's type system and Optional.none abstraction because there is no way to subvert it legally. All of this was basically a long-winded way of saying that yes, Swift's nil is actually NULL, but the type system makes sure that you don't get a "bare NULL" which lets you pretend like the Optional enumeration abstraction works but under the hood, and semantically, it's the same thing as NULL.
Whether null/NULL is a good a idea or not (I like it, just yesterday it saved my ass) it saddens me that more and more articles I stumble upon are made to criticize technologies but not to talk about solutions or innovations
In VS with resharper I get "Possible null exception" as warning, but null exception is runtime thing and not compile time, so I don't see how it is tooling problem. Compiler does not know your user did not fill in some field in form.
This tidbit gets a ton of mileage but I think it's overrated. There are a lot of unsafe shortcuts we take to get better ergonomics and NULL is one of them.
I think it's a bit unlikely we'll fully get rid of null, but we can get rid of some of the pitfalls. TypeScript for example pretty much fixes the problem, by enforcing you check for null when needed, though TypeScript takes a handful of other soundness shortcuts. Go makes null less harmful by treating nil pointers like empty values by convention.
I would recommend looking Haskell's Maybe and Rust's Option type to get a better idea of how this can be solved -- and how this article isn't really overrated (just commonly misunderstood).
They allow for explicit NULL-ness (which is a necessary concept) without falling into the trap of making everything implicitly possibly NULL. And when NULL-ness is explicit you are then forced to explicitly handle it in order for your program to compile (in the case of Haskell and Rust).
I dunno why everyone's assuming I don't know about the common solutions to the problem. TypeScript does explicit nullness without needing monads, and I actually mentioned that one. I still disagree that this is not overrated.
Go's idea of nil, for example, seems OK to me, and the language would need to be way more complex to fix it. For example, it would need a type system with explicit nullness, or maybe even actual generics. But it mostly doesn't matter because doing things with nil doesn't crash nearly as much in Go. Like a nil slice just acts like an empty slice. You can even append to nil and it returns a non nil slice. You can call methods on nil. Etc.
The trouble with getting rid of nil to me is that it requires you to either have values at all times, or deal with the possibility that you don't at all times. Go has the very very nice property that you can initialize any type to a zero value and it should work as an "empty" object. Pointers without nullability don't have a zero value. Fixing nulls at the cost of getting rid of Go's properties for zero values would not be worth it.
Don't get me wrong, Go does nil a lot better than some other languages (being able to call methods on a nil is sometimes a good thing depending on how your methods handle it -- most methods don't handle it well at all). The fact that even most map operations (access and deletion) also "just work" is really useful.
But I think you're over-selling the zero values feature of Go. It is very rare to see third-party libraries that have zero values which do anything but cause a NPE when you try to use them -- mainly because they embed pointers and then you have the same implicit NULL-ness that causes NPEs everywhere. It is great that the core language managed to get zero values right in most cases, but it's far from being as wide-spread as you might hope.
Also (nil interface != nil pointer that fulfills interface) is a very common mistake I see in Go code, and while it's not necessarily related to the existence of a nil value it is still related to the general concept of nil in Go.
[And on the TypeScript comment -- you don't really need monads for Option<> or Maybe types. You just need algebraic types -- and TypeScript has those. Haskell does use Monads for Maybe, but that's because Haskell has many other type-theory things that make it necessary to support using Maybe as a monad.]
I'm not overselling zero values in Go. Simply try to envision the cascading consequences on the language if you removed the zero values; no existing Go code would work, and I think the language would need to shift so much that even hello world couldn't be automatically translated to such a language. All to prevent a single type of runtime error among many, one that Go developers are not complaining about the way that Java, JavaScript, C developers have.
Nobody is arguing that Go should have algebraic types and ditch zero values, so I don't know why you're harping on this point. Now -- it would be somewhat nice because errors would be much more reasonable to handle (the new "check" proposal is okay but still quite flawed) but you're right that it would either be far too complicated or old code wouldn't work anymore. Go has already made it's bed when it comes to nil values, but that doesn't mean that all new languages should follow suit -- because Go's nil handling isn't all sunshine and roses (nil interfaces -- for obvious reasons -- cause NPEs).
As I've said, Go does nil basically as well as you can without having algebraic types. But given the semi-anecdotal evidence that I've definitely seen my fair share of NPEs in production Go code in the past 5 years, it's clear that it's not sufficient.
I've had I believe a single NPE in Go in production for three years. It's a blip on the radar. If you are testing your code, how do you even get an NPE in production? Seriously, it should be rare. I've actually had more trouble with channels than pointers in Go.
Also, if the situation is really so bad.... How come no one cares? I saw no Go 2 proposal to fix this situation, just generics, less boilerplate for error handling. But nothing about nil pointers. It's not at the top of anyone's list. Generally, people feel comfortable with nil in Go in ways they don't in JS and C.
I will take that for fact even though certainly you disagree. So why do people feel more comfortable with nil in Go? Because nil is not an error.
In C and JS, there are cases where nil is treated as an error. For example, getElementById returns null when it finds nothing. If you designed this API in Go, you'd instead return nil, PLUS an error (or Perhaps, a bool, if no other possible errors exist.) You can argue semantics but in Go it's generally held that if you aren't returning what the user wanted you should return an error. Exceptions to that exist but probably mostly just string manipulation functions.
This convention is so strong, though, that it nearly eliminates nil pointer errors caused by edge cases. Most nil pointer errors you DO hit in Go are:
1. Set on nil map
2. Send on nil channel
3. Failing to check error
The thing is, if you are writing production code, and writing tests for your production code, this shouldn't even make it to your code repository. It's virtually a non issue.
Even better; in C and C++, when you hit a null pointer, you don't get an NPE. You get a segfault. Everything dies. Kaput. Go is obviously not alone in not having this issue, but it's surely worth noting that it doesn't.
Go is not going to die some day because it's "not safe enough" - it's absolutely safe enough to write reliable code. The difference is, writing reliable code in Go is easy because for the most part, all you have to do is follow conventions and unit test. Same cannot be said about C and JS where you will inevitably get blindsided by sharp edges.
If you are in a situation where you can't have runtime errors, it's a poor fit. I don't know personally any developers that are in this situation. If your code doesn't put lives on the line, its OK to have a runtime error. Most of your real outages will be due to other bugs, and probably more of then will be due to other people's bugs, natural disasters, human operator error, bad configuration pushes, etc.
If you really think Go NPEs are even a significant portion of Go reliability issues I'm gonna need more evidence.
> If you really think Go NPEs are even a significant portion of Go reliability issues I'm gonna need more evidence.
Don't get me wrong, there are many other Go reliability issues. I just don't agree with hand-waving it away by saying that "you should have tested for it". I'm very in favour of testing (umoci is arguably the most rigorously-tested container image format tool, and it's written in Go) but I think that tests shouldn't be used as the solution for safety problems. The logical conclusion of such a view is that any language can be reasonably safe if you have enough tests -- and while this is true (just look at SQLite) it's hardly practical to replicate the degree of testing that SQLite does for every C project. In order for everyone to benefit, safety needs to be built in (and Go does have a lot of safety built in).
> Go is not going to die some day because it's "not safe enough"
The very first post I made in this thread was that I believe that this claim, that null is the biggest mistake in computer science, is way overblown.
To this point, I bring up Go because Go is an example of a language where NPEs are basically a non-issue. There's not much to say there, if we still disagree on this point I'm not getting anywhere and I'm just going to give up.
Following that logic, though, it feels inappropriate that null pointers have this ridiculous stigma compared to other runtime errors. Would anyone care about Rust if it's only promise were to get rid of null pointer errors? I'd argue if rust still had NPEs but effectively solved concurrency issues it would be exactly as popular today.
I'm not claiming there's no value in alternatives to null. I am absolutely disputing the idea that null is the biggest mistake in computer science. Full stop, absolutely unconvinced. I can think of a lot of things I'd consider much worse.
I still prefer more runtime safety over less, but there is a balance to be had too.
But nobody's talking about "getting rid of nil" wholesale. They're talking about getting rid of null/nil reference exceptions via the mechanisms of non-nullability (having the option to define some variables as non-nullable) and null guards (a compiler which forces a null check before any operation which requires a non-null value). This way you still have null, but with compile-time guarantees that you'll never get null when you didn't expect it.
That's the same as the TypeScript approach, but its never going to happen in Go because of the relatively small gain for a massive jump in complexity in the compiler.
It's the worst mistake because it made you believe that its atrocious ergonomics are actually superior to more sensible solutions. Implicit nullability doesn't really save you any null checks. It just makes it possible to forget necessary checks.
It was fine to design a language with nullable pointers in the 70s. It's unacceptable nowadays. nil in Go is a major mistake.
> Implicit nullability doesn't really save you any null checks.
It sort of does, because explicit nullability forces you to do many redundant null checks when you actually knew that something could not possibly be null.
Okay. So let's say we get rid of nil in Go. Now, structs with pointers have no zero value. Slices and maps have no zero value. Funcs have no zero value. Reflect can no longer create objects because it can't possibly enforce that you initialize the pointers. Functions that return either an error or a value now need a new pattern, probably requiring generics or another special type. Map access needs to return this special type.
Did we win? Did that make Go better? Fuck no. Most people aren't frequently hitting nil pointer errors in Go because unlike C the behavior is a lot more reasonable and the conventions a lot simpler. And by the way, we didn't fix all the runtime errors. Nil pointers are just one possible runtime error. How about out of bounds array access, memory exhaustion, race conditions?
And yeah, I get that you can also fix all of those things, which is then called Rust. But we don't need another Rust, Rust is a fine Rust. Go has, imo, much better ergonomics and most of the time it's just fine for what I'm doing. Like, writing small to medium size servers and utilities in Go has rarely been a regretful experience. And, even if we had no runtime errors we would still need unit testing to ensure our components are functioning correctly. So, most of the time I'm aware of when my code has runtime errors anyways.
Getting rid of null is not magic. It does not get rid of all runtime errors. And yes, it does impact ergonomics. I will take Go zero values at the cost of nil pointers, every day.
A zero value is much better than undefined value, I'll grant you that. I prefer the forced initialization approach (Haskell, presumably Rust and many others). If I add a new field, I want to know where I need to populate it. Or if you must, maybe a default value defined on the struct (perhaps that's also "considered harmful" for reasons I can't think of at the moment).
But it seems you prefer the ergonomics of default-zero. I don't get it, but I can't argue with preference.
Easy: default zero is simple. It's predictable behavior. It's consistent.
By convention, you should design your code to also treat zero values as empty. In Go, the zero value of bytes.Buffer is a ready to use, empty buffer.
If you drop default zero, you lose a lot of convenience and gain a lot of ceremony. It's not the end of the world, but neither is the null pointer error. It's just another runtime error. Just like divide by zero.
I totally disagree - NULL gives much worse ergonomics all-around. While modifying code, I'm constantly afraid of whether the value I'm accessing could be NULL. Most SQL schemas are filled with "NOT NULL" to the point of ridiculousness, and most Java methods that I've seen tend to have @NotNull used everywhere too. Not having NULL gives you a lot of confidence when reading and writing code, by guaranteeing that your object does indeed exist.
NULL is a convenient way to map singularities in your model of the problem.
I have on a few occasions tried to write NULL-less code, and it adds a good bit of work.
- model all possible states for a value
- determine appropriate default actions for all types
- meaningful place-holder values
It's a good exercise, and I think more code should be written this way, but - as an Engineer I'm trying to model just enough of the problem to solve it. I'm not trying to simulate every possible outcome in that domain.
Certain corners of your problem simply don't need to be modeled, and what's more the effort needed to model them can just be too much.
NULL is a great way to just throw up your hands and go "I don't know and I don't care". Much as when modelling a physical system singularities typically represent phenomena that the model doesn't take account of, so it goes with NULL. It simply says "Don't Go There".
Have you ever dealt with the Maybe(Haskell)/Option(F#) types? If not, then you don't understand what's wrong with NULL and how to easily avoid it without much work.
I find Maybe a bad idea. It forces me to write denormalized code when I know that something is not NULL. It's not possible to specify this knowledge as a data structure since data structures are static but context is dynamic. I much prefer the simple NULL sentinel that blows up like an assertion when I made a mistake. That said, there's not very often a need for NULL at all if you structure the code correctly.
> I much prefer the simple NULL sentinel that blows up like an assertion when I made a mistake.
Haskell, for instance, has the 'fromJust :: Maybe A -> A' function that allows you to do just that. It unpacks the Maybe typed value and throws a runtime error if it fails.
Yes. Most Haskellers will sneer at it, while personally I think it's the right thing to do because it conveys the programmer's ideas about invariants. But syntactically an explicit unwrapping function is still a lot of noise. Simple null pointers as we have in C, with an unmapped segment at address zero so that it throws a segmentation fault, are much better.
> but are you saying that throwing a segfault can be a good thing?
Sure, what's bad about it? A logic bug was detected, so the program should be terminated. Or how do you intend to continue?
Segfault is not so different from what happens if you do "fromJust Nothing" in Haskell or get a NullPointerException in Java. You can even write a handler for the segfault, but I guess that's rarely a good idea.
> Sure, what's bad about it? A logic bug was detected, so the program should be terminated. Or how do you intend to continue?
I intend to not have the logic bug in the first place, by encoding my invariants in the type system.
If you "know" that the value is present rather than absent, you must have a reason for knowing it, so explain that reason to the compiler. E.g. maybe you took the first element of a list value that you know is non-empty - so maybe you need to change the type of that value to a non-empty list type. That way the compiler can check your reasoning for you, and will catch the cases where you thought you "knew" but were actually wrong.
the way I program that is nothing but a pipe dream.
> If you "know" that the value is present rather than absent, you must have a reason for knowing it, so explain that reason to the compiler.
I might know that it exists for example because it is computed in a post-processing step after a first stage but before a second stage. So it exists in the second stage but not in the first. Relying on global data (which I won't give up) makes it practically impossible to encode that the data is not there in the first stage.
And that's not a problem at all. I simply don't access that data table in the first stage... Trying to explain my processing strategy to a compiler would amount to headaches and no benefits.
> I might know that it exists for example because it is computed in a post-processing step after a first stage but before a second stage. So it exists in the second stage but not in the first.
So the first stage could create a handle to it, or even just a phantom "witness" that you treat as proof that the value is present.
> And that's not a problem at all. I simply don't access that data table in the first stage... Trying to explain my processing strategy to a compiler would amount to headaches and no benefits.
Shrug. I found that errors would make it into production, because human vigilance is always fallible. And the level of testing that I needed to adopt to catch errors was a lot more effort than using a type system.
Accesses to unallocated global data is the type of errors that you typically hit on the first test run. Another example would be function pointers loaded from DLLs.
I don't think type systems help all that much. Type + instead of -, and you're out of luck.
> Accesses to unallocated global data is the type of errors that you typically hit on the first test run.
Depends what conditions cause it; the hard part is being sure that every possible code path through the first stage will initialise the data, even the rare ones like cases where some things time out but not others.
> I don't think type systems help all that much. Type + instead of -, and you're out of luck.
Not my experience at all - what do you mean? If you declare a type as covariant instead of contravariant or vice versa, you'll almost certainly get errors when you come to use it.
> 1) Pretty easy to guarantee if main looks like stage1(); stage2(); stage3(); etc.
You can only use the global program order once, I'd rather save it to spend on something more important. val result1 = stage1(); val result2 = stage2(result1); ... means my code dependencies reflect my data dependencies and I'll never get confused about what needs what or comes before or after what (or the compiler will catch me if I do), so I can refactor fearlessly.
> 2) Change a plus for a minus and it is still an int.
True. If you get your core business logic wrong then types won't help you with that (though FWIW I'd argue that it's worth having a distinct type for natural numbers, in which case - and + return different types). But I found that at least 80% of production bugs weren't errors in core business logic but rather "silly little things": nulls, uncaught exceptions, transposed fields... and types catch those more cheaply cheaper than any alternative I've seen.
Replying to your lower comment (the coffee has kicked in):
The situation you describe is one where a null really is an unrecoverable error, and the program should terminate. That is the one case where it makes sense to just let a NPE happen.
However, the vast majority of time, a null is just an absence of value, and does not signify an unrecoverable error. Those are the kind of situations that an Option/Maybe helps with, since it doesn't let you forget to handle the null case.
Even if a null value returned from a function is abnormal, and the program shouldn't continue, an Option is still going to be better most of the time. After all, you probably have connections and stuff you want to cleanly terminate before shutting the program down.
If you know something can't be null, then don't use an option. Simple as that. For example, a SQL library can return a non-nullable column of String as just a String, not an Option[String]. Thus, you actually get a solid distinction that you don't get with null pointers.
There's no reason to include sentinels that will randomly blow up your program.
No. The point is that the data structure can't know if there's a NULL since the data structure is static. Context is dynamic. Code is dynamic as well, and it can know that some things must exist based on other dynamic conditions.
So this "solid" distinction often is just noise and actually blurs the intention of the programmer: An explicit unwrap is required syntactically while it should not be required semantically because really the option data is not an option but a requirement in certain contexts.
If it is a requirement for something not to be null, unwrap the option before you send to it to the part of the program that can't accept nulls, and deal with the case of None in a sane way and in a predetermined place. Then you don't have to worry about unwrapping in the rest of the code. You can escape from Option. It's not like IO. You just have to check for None if you want to get something out, as you should.
In this fashion, you have type safety everywhere, and you deal with the case of a missing value in a predictible way, in a single spot.
I see these as a more sophisticated way of dealing with NULL. It allows me to define alternative default behaviour beyond just throwing an undeclared exception.
They are still NULLs however under the hood and I still need to do the work of defining what I want to happen when they occur. It's just neater.
You can conceptualize the Maybe based on the NULL, but there's no point in the compilation or runtime in which they actually become NULLs, it's just a regular container value.
Which is the same as if I go and ensure a default "noop" value is assigned ... it's logically a NULL. I don't know anything about it except that it hasn't been assigned.
It's not a NULL. A NULL is a value that is considered by the type system to be a valid instance of a given type, except it doesn't actually fulfill the type's contract. A Maybe is a completely different type, much like a list or a map or a tree.
Well at this stage you're just being narrow minded.
I think if you read back over this thread it should be clear what I mean.
I proposed that NULL has a purpose. You proposed that Option obviates this. I stress that it's just a neater way to manage the conditions you don't need to model. You point out that in terms of implementation it's different, where I explain that still, logically it's the same thing.
Of course NULL has a very specific meaning in the structure of the language, and when you start using things like Options it makes managing NULL easier, but it's a rose by another name, gift-wrapped, and bundled with some plant food.
Logically however, at the point where you're modelling your problem it's the same.
They're not null. You might use them to represent the same thing that you use null to represent, but the type system won't let you use them in expressions that aren't explicitly built to accept them.
> I have on a few occasions tried to write NULL-less code, and it adds a good bit of work.
Really depends on the language. On Java / C#, it's hard to avoid null-checking a lot of stuff. In C++ you will never encounter a null std::string outside of code written by indian students learning on Turbo C++ 3.5. So you don't need to check for anything: if it's a value, it exists.
Do people generally agree that Java Optional == Scala Maybe / Haskell's Maybe?
Java's Optional seems fundamentally flawed in that Java allows any reference to hold a null value and Optional can still throw a NPE when calling isPresent on it so it still gives people a footgun.
Yes I was going to mention that video in which Rich makes an important point in my opinion: database tables, or objects, or structs, still live within the Place Oriented Programming (PLOP) mindset. That mindset was born in a time where disk and RAM were the expensive resource therefore update-in-place was the default. I insist on the "in-place": you need to know where something is so you can update it. The downside of PLOP is that if you have no value for one of the slot in your generic form (be it a table, object or struct) then what can you put there?
The alternative is to use data shapes that do not require something to be in a certain place. Hence the use of maps as the most basic data shape: you either have an entry in it, or you don't, no need to have a null entry. Expanding that thinking to databases, and you realise tables is not the right aggregate, instead you need to go one level down to something that datomic calls datom, or RDF calls a fact.
To summarise, PLOP forms that package together a set of slots to be filled magnify the issue of NULL/null/nil. Instead make the slot your primary unit of composition and make sure you use aggregation of slots that does not force you to have slots filled with a null value when there is no value in the first place.
Yes, 'maybe not' is very relevant to this discussion, but few people seem to agree with my understanding of what he says about the right solution:
Optionality doesn't fit in the type system / schema, because it's context dependent. For some functions, one subset of the data is needed, for others a different subset. Trying to mash it into the type system / schema is just fundamentally misguided.
Yes, he's rather explicit in saying Maybe is a poor tool.
I'll have to watch the talk a second time to be sure, but I'm not sure he proposes any solution at the level of type systems. Not using Maybe or using Union is not what he is advocating.
For him (and me too) types are the wrong thing to put data in because, among other things, it forces you back into PLOP. His point is to remove entirely the need to fill slots with nothing.
Obviously the talk is more about specs than types. While tactfully avoiding the debate around types, he's still starting the talk with types to help those that are only there to decomplect their thinking.
> Java's Optional seems fundamentally flawed in that Java allows any reference to hold a null value and Optional can still throw a NPE when calling isPresent on it so it still gives people a footgun.
The existence of Optional makes it possible to migrate away from using "null". E.g. Map#get could be replaced with a method that returns Optional.
Calling isPresent is the wrong way to use an Optional. But yes, it's an ordinary Java value, it can be null as long as Java-the-language permits null. That's not a problem with Optional, it's a problem with Java, and introducing Optional as a plain old normal Java class is the first step to fixing it.
Isn't there an inherent need in programming to express an explicit "nothing" value? Coming from Python and JS, I never found None/null to be much of a problem. I in fact like the distinction of null and undefined in JS. Using null allows you to distinguish from the accidental undefined.
The key word here is explicit. Explicit Maybe/Optional types from Haskell, Swift or Rust are wonderful. After my experience with Elm and Swift, the mere thought of going back to implicit nullability everywhere hurts.
No, there’s no inherent need for every variable to allow a nothing.
There is a need for a nothing value in many cases and languages without an implicit null, such as Haskell, F# and Rust, employ an ‘option’ type. It’s a dedicated type that either contains a value or nothing. It forces you to declare when you expect a potential null and to check for it.
> Isn't there an inherent need in programming to express an explicit "nothing" value?
In numerical calculus: yes, most certainly. What do you expect the result of log(-1) to be? The alternative is to use specially tagged particular numbers as "no-data", and pray so that they do not appear naturally as the result of computations.
Some systems already do that. C#'s Double type includes Double.NaN, Double.NegativeInfinity, and Double.PositiveInfinity. Math.Log(-1) does indeed return NaN.
Technically, you could force the value to be one constrained to a valid range, rather then augmenting the domain, but this is a lot of work and maybe not worth it for pracial use.
The major criticism towards NULL's does not apply to dynamic languages. The problem in languages like Java is that nullability is not represented in the type. This criticism is not relevant in a language without static type checking. The criticism is also not relevant in a language like TypeScript where nulls are specified in the type.
In short, the problem is not nulls per se, the problem is static type systems which does not allow you to specify if a certain value can be null or not.
But I hereby predict that soon people are going to declare that "nulls are bad" and eliminate them even in languages where they are totally fine.
The OP specifically calls out ways that nulls cause problems in dynamic languages. In fact, they're very similar to the problems nulls cause in statically typed languages, just ignoring some of them because danger is already priced in with a dynamic language.
Dynamically typed programs are usually informally "duck typed," since it's impossible to do something meaningful with truly arbitrary types most of the time. But just like in statically typed languages, there's always the very real possibility that your duck will instead be a null — and the bug is not the function returning a non-duck, it's you ever assuming you have something that quacks like a duck.
The way Clojure handles nulls is ideal for a dynamically typed language. In Clojure, a nil is a perfectly valid object, and it is also a sequence (it's the empty list equivalent). That means that most standard library functions have no problems letting nil flow through them, returning nil. It winds up being quite similar to monadic Option/Maybe, although with no guarantees.
Well in a dynamically typed language there is also the possibility than something you expect to be an integer is in fact a string. Null or Nil or None is not different in this respect than any other type.
But of course you can make bugs involving nulls. The OP shows an example in a dynamic language where null is returned instead of the regular value if a lookup key is not found. Obviously this is ambiguous when the found value can also be null. But the problem is not the null per se, the problem is using a sentinel value to indicate a special condition when the same value is also a legitimate regular value. This is just bad API design.
Because it's a 0 overhead abstraction and guard against the case when you (or a coworker) forget to check for null. Are we seriously gonna pretend like you never caused a null pointer exception?
std::optional doesn't do that though, you can deref an empty optional and the result's the same as deref'ing an empty unique_ptr or a null pointer: UB.
Even in dynamic languages, it's generally bad practice to have functions that return multiple possible types, and you are often better served by returning a more proper "null object" that accepts the same messages/methods as what would normally be returned. But for the most part, the problem people have with null is related to compile time checks and it not being opt in, which is unrelated to dynamic languages where every return type cold contain... whatever.
I agree that returning the same type, if possible, is better, since you can model it so that one specific value (zero, empty string, etc.) expresses the "nothing" accordingly. But I have found that several other developers don't share this view and prefer functions to return false, null, etc. instead of a value in the same space as the normal return type.
So how does NaN in Python and NA in R relate to NULL? I know Python has the None type, but it's not the same as NaN. One of the most annoying things in Numpy is that there is no way to indicate that an integer value is "missing", similar to NaN for floats. In R both integers and strings can be NA (if I remember correctly). So for numeric types at least, there is definitely the need to somehow indicate that a value is "missing".
NaN is not the same as None/null, and using it that way is asking for bugs.
None/null is for missing/unknown values (which may be a reasonable thing in your domain), while NaN is for the result of an illegal/undefined operation (which is definitely a bug, such as division by 0).
> NULL is a value that is not a value. And that’s a problem.
The problem isn't NULL, it's languages not enforcing the necessary checks for the "no data" condition. Option can still be NULL ("None" in rust), wrapping NULL in a struct doesn't provide any safety. The safety of Option wrapper types is from the other language features (like rust's "match") and a stricter compiler that forces the programmer to write the NULL check.
NULL would be fine if C required you to write this:
foo_t *maybe_get_foo(/*...*/) {
if (/*foo_is_available*/) {
return foo;
} else {
return NULL;
}
}
foo_t *f = maybe_get_foo();
if (!f) { /*...*/ } // REQUIRED or compile error
do_something(f->bar); // only allowed after NULL check
Obviously implementing that requirement would be difficult in C. Languages like Rust were designed with enforcement features (match + None, much stronger type/borrow checking), but lets your have "a value that is not a value".
> The problem isn't NULL, it's languages not enforcing the necessary checks for the "no data" condition.
Talking about "NULL" pretty much implies that. When Tony Hoare talks about null references, it's about every reference being nullable in languages like Java or C#, not about the ability to conceptually wrap/opt non-nullable references in a nullability thingie.
It isn't. There exists a common monad that solves this problem but a wrapper type like Option or Maybe need not be a monad. For example, `Nullable<T>` in C# is not a monad.
No. The thingie may or may not be monadic, but I'm using thingie because it could be a "library" sum type, or it could be a magical-ish builtin, or it could be some other mechanism.
Not sure why I am getting downvotes for this. The parent proposed a language feature to support an enforced code execution when a return value was not available:
foo_t *maybe_get_foo(/*...*/) {
if (/*foo_is_available*/) {
return foo;
} else {
return NULL;
}
}
foo_t *f = maybe_get_foo();
if (!f) { /*...*/ } // REQUIRED or compile error
do_something(f->bar); // only allowed after NULL check
And I was pointing out that C++ exceptions do exactly that.
You might instead argue that the problem is allowing checks for the null condition. If you make comparing a pointer with null crash, or just remove the keyword to make its use non-idiomatic (or only allow it as special initialization syntax), people won’t casually use null to mean something. It’ll serve its role as a pointer value that crashes cleanly if you dereference it.
No, the problem with null is the inability to enforce, at the type level, that a particular value is not null. C simply does not have a type for "guaranteed valid pointer to x".
With the required extra accessor functions. Of course in C, we don't have generics, data hiding or other nice features, so we have to build a wall of conventions around our types instead. Types with invariants are totally possible in C. We just don't have a way to automatically enforce them. It's C, after all.
It can only really be done on a per codebase level, really.
By which I mean, in my highly sensitive and correct program xyz I can (say) abstract all access to a type (A struct?) through a macro which would contain null checking/assert-ing. This could work perfectly well and even be static checked for (in principle).
However, if I tried to package this type up in a library it all basically falls flat - or at least it would be very easily broken.
Doing this like this is to static analysis(/The Type system) as foreplay is to sex.
> Option can still be NULL ("None" in rust), wrapping NULL in a struct doesn't provide any safety. The safety of Option wrapper types is from the other language features (like rust's "match")
The advantage here (especially true in Haskell) is that you can use monadic error handling to make this far more pleasant.
That's a secondary nicety (alongside the ability to make everything "nullable" not just the references/pointers subset), the primary advantage is that:
1. it clearly separates "nullable" and "non-nullable" providing better modelling tools
2. the compiler assists/mandates null-checking, providing better type-safety
Even Rust doesn't have the strictness in your comment. It's perfectly fine by the compiler to make use of `x.unwrap()`: if x is None (or Err, in the case of Result), you'll just get a panic at runtime. The features you note are superior to C's offering, but purely optional.
There's a fundamental difference between the Rust approach of the library providing a function for opting-in to potential crashes and the C/Java approach of not distinguishing that case at all. The programmer still is forced to write a null check, it's just a check that crashes the program.
I largely agree, but that would get very tedious because there's no way to create a pointer type that is guaranteed to be non-null in C. You'd end up having to do a lot of unnecessary checks because of that.
People say optionals are the solution, but the way I see it it's the other way around. Pointers types that allow NULL are basically optionals, and the problem is that we use them everywhere, even for things that are not optional. What we are missing are pointer types for things that are not optional.
And the pointer type with a guaranteed value needs to be as easy to use as the nullable pointer type, if not easier. Otherwise it won't always be used when it should be.
Not very difficult. Just provide a not_null pointer attribute, just like const. Then require that all dereferenced pointers must have the not_null attribute. Problem solved.
(Other note: C++ has a not_null pointer-like type: it's references. Unfortunately, C++ references cannot be reseated, which makes wholesale replacement of pointers not feasible. Plus, the language doesn't actually forces you to check pointers before assigning to a reference.)
I mean it's been a solved problem since the 70s if not earlier, the problem has always been uptake. And that is not solved e.g. C++ recently introduced std::optional, which is not a type-safe version of a null pointer but is instead a null pointer wrapper for value types.
null is often used in relational databases to be semantically equivalent to "info missing". I get that this use of null isn't ideal.
However there are quite a lot of cases where it is semantically equivalent to "not applicable"; while some of these cases can be avoided with a restructure, some can't, and in either case I don't see a compelling argument to do so.
e.g. a parent_id column that is null for top-level objects in a hierarchy. Restructuring the database to avoid this seems like moving to an unintuitive paradigm in pursuit of rule-following for its own sake.
I think it would be nice if `NOT NULL` was set by default on columns. However there are a lot of legitimate use cases that can't be (practically) solved by restructuring.
Data can be incomplete. Maybe only because it is not (yet) known. If NULL values were impossible it would create the need for one additional table with a foreign key relationship for every attribute that can be independently NULL. Sometimes this pattern is a good idea, but I don't think I can be convinced it should be the only possible solution.
"Explicit" defaults are still quite implicit. Usually what I want is an error if I fail to provide a required value, not any kind of silent default, null or otherwise.
How do you propose we represent the absence of data? At least in SQL "logically missing data" and "missing data in memory" are properly divorced, unlike many languages in which the latter is abused and conflated with the former.
First of all, Codd's early work didn't propose it because Codd's propositions were idyllic and rooted in mathematics and not software engineering. In Codd's world, you would never create a FirstName/GivenName and a LastName/Surname field. You'd just create a Name field. The fact that you might want to sort by last name or the fact that someone named "Alice Taylor Smith" has a surname of "Smith" while "Robert Taylor Smith" has a surname of "Taylor Smith" isn't relevant. In Codd's world, you'd just define Name as a Name type, and the Name type itself would know everything about the entire domain of names. It would be about as complicated as working with a DATETIME. The same would be true for an Address type. Such a type would support both Western and Japanese addresses, which are wildly different. Codd doesn't need NULL because he has complex types which perfectly and comprehensively represent their domains. That's not realistic. Parsing Names and Addresses for even a single culture is notoriously difficult.
Second of all, Codd's later major work, The Relational Model for Database Management, includes his Twelve Rules (numbered 0 to 12) for database design in order for a DBMS to be considered relational[0]. Rule number 3 is:
> Rule 3: Systematic treatment of null values:
> Null values (distinct from the empty character string or a string of blank characters and distinct from zero or any other number) are supported in fully relational DBMS for representing missing information and inapplicable information in a systematic way, independent of data type.
So Codd clearly thought that nulls were essential.
Most languages do. Java will always initialise a non-initialised value to NULL for instance (EDIT or 0 or false for primitives).
It's simply a reality of how computers operate that when you allocate a piece of memory (a variable) it will have something in it that you'll need to clear or initialise.
I've made my peace with null. Null is basically just an implicit
assert(valid(x))
before every time you call a method on x. Similary, I think of exceptions as explicit "crash-unless-caught" commands.
If you write your program with the "blow up early" mentality anway, or use static checking tools and a bit of discipline, I've found that null looses it's terror.
If it's C, it's far more terrifying than that.. there is no assertion! just a vauge threat that something will go wrong if you stick a null in there, with no guarantees and no checks.
Sure, null can be a non-problem for the low, low price of 3 or 4 extra lines on each function. But those extra lines distort your program architecture, pulling you away (perhaps without even noticing) from short, composable functions.
In market terms, sir, you've entered the capitulation phase haha. It's actually not correct to say that accessing null will always blow up. In embedded systems without memory protection address 0 may well contain valid data, usually a vector table. In WASM address 0 is totally valid also, if I'm not mistaken, as memory is represented as a big ol' array with an offset and checking for 0 would be too inefficient.
> I've made my peace with null. Null is basically just an implicit
That very much depends on the language:
1. it can be a compile-time error (Swift)
2. it can be a runtime error (java, C#)
3. it can depend on what you're actually using (Python, Ruby, and on runtime extensions you might have loaded in the latter case)
4. it can be a no-op (objective-c)
5. it can depend on the combination of platform, compiler and surrounding code going from a segfault to deleting your program's security and/or causality (C, C++)
6. it can depend on the implementation and exact codepaths (Go)
To someone who has been using Asm and C for decades, these arguments just make no sense. Reading this article reminds me of the arguments against pointers, another thing that's frequently criticised by those who don't actually understand how computers work and try to "solve" problems by merely slathering everything in thicker and thicker layers of leaky abstraction. It's not far from "goto considered harmful" either.
any reference can be null, and calling a method on null produces a NullPointerException.
...which immediately tells you to go fix the code.
There are many times when it doesn’t make sense to have a null. Unfortunately, if the language permits anything to be null, well, anything can be null.
That's not an argument. See above.
3. NULL is a special-case
...because it indicates the absence of a value, which is a special case.
though it throws a NullPointerException when run.
...and the cause is obvious. I'm not even a regular Java user (and don't much like the language myself, but for other reasons) and I know the difference between the Boxed types and the regular ones.
NULL is difficult to debug
Seriously? A "nullpo crash" is one of the more trivial things to debug, because it's very distinctive and makes it easy to trace the value back (0 stands out; other addresses, not so much.) What's actually hard to debug? Extraneous null checks that silently cause failures elsewhere.
The proposed "solution" is straightforward, but if you reserve the special null value to indicate absence then you can make do with just one value instead of a pair, of which half the time half of the value is completely useless. If you can check for absence/null, you will have no problems using Maybe/Optional. If you can't, Maybe/Optional won't help you anyway --- because it's ultimately the same thing, using a value without checking for its absence.
A simple, safe way of tracking nulls like option is "CS theory going off the deep end?"
Implicitly allowing all code to return nothing, and manually trying to remember what can return null and what can't, and checking that value, is incredibly error prone. It's really crazy that this has been the dominant way of handling the problem for many decades when their is a dead-simple way of ensuring it can't happen.
Null pointer errors, contrary to many claims, show up in production code all the time. Eliminating them is of huge value.
The NULL pointer errors yo're referring to in most cases resource issues. i.e. malloc returning NULL.
This is not the source of the vast majority of pointer errors.
Checking for (and trapping) NULL pointer dereferences is trivial, what is more difficult is the rest of the pointer range that doesn't get checked but is equally invalid, i.e. the other 4-billion (32-bit) possibilities.
Non-NULL-pointer checks are much more important than NULL checks.
The world of pointer issues is very much greater than "ASSERT(ptr!=NULL)".
...and as for correct error-recovery (not error-detection), well, don't get me started.
>This is not the source of the vast majority of pointer errors.
>Checking for (and trapping) NULL pointer dereferences is trivial, what is more difficult is the rest of the pointer range that doesn't get checked but is equally invalid, i.e. the other 4-billion (32-bit) possibilities.
I think we write vastly different types of software. I can assure that that null-related errors are extremely common in situations besides resource issues. If it were just a resource-related problem, garbage-collected languages would almost never have issues, yet Java is infamous for NPEs. In Scala, where Options are ubiquitous, I've literally never had a single NPE.
It is very common for libraries to return null just to represent the absense of a result (ex: a row returned from a SQL query has no value for a column). That sort of thing means you have NPEs wholly unrelated to malloc or anything similar. These nulls are expected under normal program operation. They aren't errors. So, it's crazy to not to let the type system assist you in checking for nulls, so you don't forget and wind up with a NPE.
Pointer issues (that I was referring to) and a failure indication.
The most trivial pointer issue is a NULL pointer. This is such a trivial issue to catch its hardly even an error, yet people use that case as the exemplar for NULL issues.
detecting (and handling) failures on the other hand is very much different and more in the spirit of what the option-type arguments are about.
In that case, the difficulty is not in detecting the error (that option-types will help with) but the application-level recovery. that is nothing that the language aid you with, its system-design and architecture related.
Basically, its the wrong issue to be thinking about.
>The most trivial pointer issue is a NULL pointer. This is such a trivial issue to catch its hardly even an error, yet people use that case as the exemplar for NULL issues.
How can you claim that NPEs are "hardly ever an error." NPEs are the most common error there is! They are indeed easy to catch, but you need to do so nearly everywhere, obscuring the code and introducing potential for error. There is no real, conceptual difference between something like a malloc returning null or a database query result containing a null. It is the same thing.
A null absolutely is an error if you don't catch it. By not using Options, it's vastly easier for that to happen.
But yet it is the most common type of bug, even in production. Clearly, it's not that easy to deal with for human programmers. What's the problem with letting the compiler help you?
Cannot understand this position... a non-nullable pointer is pretty much just a normal pointer but the compiler checks if you have tested for null. In rust (and I believe Swift) optional references also have the same size as normal pointer. In some version of non-nullables you only need checks for dereferencing and not for other handling.
Just look at the example on Wikipedia[0]. Tagged unions are super-simple, and make NULL completely unnecessary. NULL causes lots of headaches, while tagged unions have never caused anyone headaches, so removing NULL is kind of the obvious thing to do. In a sufficiently advanced language, such as Rust, they get optimized to equivalent code anyway, so there isn't even any performance loss.
> ...which immediately tells you to go fix the code.
Assuming your code isn't deeply nested. I've seen cases where null was triggered years after code went into production.
In that case you have to:
A) Assume value isn't null and have more readable code
B) Litter the code with null checks.
e.g.
if (a.getStuff().getValue() == "TEST")
becomes
if (a != null && a.getStuff() != null && a.getStuff().getValue() == "TEST)
Thing with Maybe/Optional you have to check for presence of None, otherwise your code won't compile. Another smart way is what C# did. Integer can't be null. Integer? can be null.
You could check every value for null, sure. But a) why would you want to? (and wouldn't it be bad for performance) and b) how would you handle it? Knowing that a value somewhere in your program was null doesn't really help you any.
The thing is, you're focusing on when you've detected that there is an issue (a crash). A lot of the issues with NULL are the fact that you can't easily detect if beforehand. It's not indicated in the types, or the syntax. That means that it's incredibly easy for a NULL issue to sneak into an uncommon branch or scenario, only to be hit in production.
> another thing that's frequently criticised by those who don't actually understand how computers work and try to "solve" problems by merely slathering everything in thicker and thicker layers of leaky abstraction
I think that you're being quite unkind. Haskell's Maybe type and Rust's Option types are very far from "leaky abstractions" and were developed by people who definitely understand how computers work. In fact, your description of them appears to indicate that you aren't really sure how they work (None doesn't take up "half of the value") -- the point of typeclasses is that cases where NULL is a reasonable value are explicit and your code won't compile if you don't handle NULL cases. Allowing NULL implicitly for many (if not all) types is where problems lie.
It also appears you're arguing that languages which don't have models that are strictly identical to the von Neumann architecture are "merely slathering everything [with] leaky abstraction". Would you argue that LISPs are just leaky abstractions?
Well, a better comparison would be Typescript with strict null type checking enabled. You just use algebraic data types to specify whether a value can be null.
Yep! It's worth restating the fact that wrapping a pointer in an Option in Rust actually takes up NO extra space, because Rust's smart enough to just optimize it back into a nullable pointer.
If your whole world is asm and C, then I take it you don't care much about type systems. Bless your heart, you lonely programmer of ephemeral software, may you be employed gainfully fixing your own bugs for decades. The saltiness if mostly for entertainment, please don't take too much offense. For everyone else working at a level of complexity where mistakes are inevitable and costly, types are an essential bicycle for the faulty minds of programmers.
The article is not arguing that we shouldn't express or model the absence of a value. It is arguing that all types having to support "no-value" case leads to error prone code that has historically cost immeasurable amount of money and much credibility and respect. If everything can be null then it takes too much effort to null check every reference, so developers routinely forget or think they know better. Instead it argues that we should model the idea a possible empty values as a separate composable type. Then you can write a large percentage of your code with a guarantee that objects/type/values are never going to be nil, while still handling that possibility in the smaller error checking parts of your code base.
One interesting anecdote is that our team, working in Swift, had to integrate a crash reporting tool and verify that it works. The challenge was that we haven't seen a runtime crash in several months in production.
> A "nullpo crash" is one of the more trivial things to debug
If it happens in your debugging environment in front of your eyes then maybe. Some of us work on software that is used by millions over decades and would never get to see any reports from a majority of crashes.
> > There are many times when it doesn’t make sense to have a null. Unfortunately, if the language permits anything to be null, well, anything can be null.
> That's not an argument. See above.
It is actually their best point, IMO. I really like how RDBMS/SQL solve this: fields hold values and you specify beforehand whether they can hold NULLs. The author is right, sometimes it does not make sense for variables to be null-able (think ID fields or usernames) but often it does (e.g. a user's avatar). Being able to indicate that would be a nice idea. C++ for example does that, as `Field x` is not nullable but `Field* x` is.
Full path coverage is much more difficult than 100% branch coverage. It's next to impossible in any non trivial codebase that wasn't designed specifically for formal verification.
> ...which immediately tells you to go fix the code.
But which code? The point where you observe the error could be many compilation units away from the code that's broken; it might be in a separate project, or even 3rd-party code.
> ...because it indicates the absence of a value, which is a special case.
Why does it need to be a special case? Is your language incapable of modelling something as simple as "maybe the presence of this kind of value, or maybe absence" with plain old ordinary, userspace values?
> Seriously? A "nullpo crash" is one of the more trivial things to debug, because it's very distinctive and makes it easy to trace the value back (0 stands out; other addresses, not so much.) What's actually hard to debug?
"Tracing the value back" is decidedly nontrivial. And totally unnecessary if you just don't allow yourself to create that kind of value in the first place.
> if you reserve the special null value to indicate absence then you can make do with just one value instead of a pair, of which half the time half of the value is completely useless.
What do you mean? If you're talking semantically, you want absence to be a different kind of thing from a value: it should be treated differently. If you're talking about runtime representation, you can pack an Option into the same space as a known-nonzero type if you want to (Rust does this), but that's an implementation detail.
(Confusing sum types with some kind of pair seems to be a common problem for programmers who haven't used anything but C; sum types are a different kind of thing and it's well worth understanding them in their own right).
> If you can check for absence/null, you will have no problems using Maybe/Optional. If you can't, Maybe/Optional won't help you anyway --- because it's ultimately the same thing, using a value without checking for its absence.
Nonsense on multiple levels. Maybes deliberately don't provide an (idiomatic) way to use them without checking. By having a Maybe type for values that can legitimately be absent, you don't have to permit values that can't be absent to be absent, and therefore you don't have to check most values - rather you handle absence of values that can be absent (the very notion of "checking" comes from a C-oriented way of thinking and isn't the idiomatic way to use maybes/options) and don't need to consider absence for things that can't be absent.
The whole point of using type systems is to prevent human errors; a "poka-yoke" for programming.
The great advantage of Maybe/Optional systems is that only some of your references have to use them. You can draw a clear boundary between the parts of the code that have to check everything, and those that can prove it's already been checked.
In assembler we have no real type annotations, but for a long time I've considered trying to design a type-checking structure-orientated assembler.
> ...because it indicates the absence of a value, which is a special case.
But that's exactly the problem -- it's special, meaning it's only useful for certain situations. An ideal type system would provide compile-time guarantees, rather than having to wait for users to report issues. A type system which A) allows you to define variables as non-nullable and B) requires a null guard before every dereference of nullable variables eliminates this entire class of bug. What on earth is wrong with that?
EDIT:
> Seriously? A "nullpo crash" is one of the more trivial things to debug
Even if this were true [0], wouldn't it be easier if such a crash was just never even possible?
[0] which it's not - all a nullpo stack trace tells you is that something important didn't happen, at some point before this
In general, it's difficult to make the case that runtime errors are preferable over compile-time errors, unless the difficulty to enable compile-time errors is significant. In the case of Optional<> as a language/DB type, I can't imagine much effort involved, except for uptake.
Especially given that it can trivially be optimized out, since the eventual assembly should very well make use of the 0x0 property. But if you can encode the guarantee in your "high-level" language, why would you not want it?
In fact, I'm not sure how anyone could imagine assert(n != null) scattered throughout the codebase is a pleasant situation, unless of course, as most do, you're skipping the safety check for unsafe reasons.
I've been using kotlin and swift. They've partly removed null with the 'maybe' feature.
So instead of calling methods on null objects the methods are just not called if the object is null.
This helps when there's a race condition, and you attempt to call a method on a null object, and then that solves itself by the same code being called again without the race condition.
But a lot of the time if the object is null and the method is not called you still have an error, but it's just not a null pointer error now.
This 'nullless' code is nice in some places, especially with UI lifecycles calling code repeatedly, but other times it just changes the type of error you debug.
Actually in Kotlin, nullability is part of the type system and denoted with a ?. So String and String? are two different types. Dereferencing a nullable type is a compile error until you do the null check. Doing that triggers a smart cast to the non nulled type. So the inferred type becomes non nullable and you don't have to do any casts. You can force this cast by using the operator !!, which you should avoid for obvious reasons but is useful with some legacy code. If you get this wrong you still get an npe.
It also provides backward compatibility with java where java types are considered nullable by default unless other wise annotated with a @NonNull. Also you get nice warnings about redundant null checks.
This provides for a lot of extra compile time safety and it largely removes the need for Maybe, Optional, and other kludges that people have been coming up with to force programmers to replace null checks with empty checks.
Nah. There will always be missing values, no matter how many layers of safety measures we wrap around the fact. Hitting a NULL in C is very unforgiving; but that's just the spirit of C, there are plenty of ways to provide a less bumpy ride in higher level languages.
My own baby, Snigl [0], uses the type system to trap runaway missing values without wrapping. Which means that you get an error as soon as you pass a NULL, rather than way down the call stack when it's used.
376 comments
[ 5.6 ms ] story [ 368 ms ] threadThat's the problem right there.
When did computer science become about hand holding? has it always been this way? Look at react. It was designed to force functional programming concepts in an OOP manner. Is the future of programming the implementation of tightly controlled interfaces with extreme type safety? I would argue thats where we are going. Things are becoming less expressive, not more.
When people with pragmatic goals want to get large teams of new programmers productive fast, and can't expect everyone to be able to fend on their own or can afford the cost of accumulated mistakes.
>Look at react. It was designed to force functional programming concepts in an OOP manner.
Whatever that means, as React has little to do with "OOP manner".
React has move to stateless components and functions over classes.
The two languages should not be conflated anymore.
In C, the literal "0" is a null pointer constant handled at the compilation stage, but casting a runtime zero is not specified to yield a null pointer (and the address zero can be perfectly valid and usable), nor are null pointers specified to be zero-valued (quite the opposite).
#include <stdio.h>
int main(int argc, char *argv[]) {
2. The specific implementation you have on hand could use zero-valued null pointers, I'm telling you what the standard doesn't say, confirmed by the C FAQ:
http://c-faq.com/null/varieties.html
http://c-faq.com/null/confusion4.html
http://c-faq.com/null/machexamp.html
3. The null pointer is not "directly convertible to a boolean 0", it's the literal 0 which expresses a null pointer: http://c-faq.com/null/ptrtest.html
- null terminated strings
- machine dependent integer widths
This is mentioned.
> machine dependent integer widths
What exactly do you dislike about this?
Or what do you even do about it?
A handful of solutions already exist: - Use a higher-level language - Java
All you need is a list of what width each type is.
> A handful of solutions already exist: - Use a higher-level language - Java
It doesn't require being "higher level". If anything it pushes code to a slightly lower level.
This is exactly the kind of minutiae that GP was bemoaning.
> If anything it pushes code to a slightly lower level
Yeah by way of higher-level abstractions ...
How is this “minutiae”? You should always know the possible range of a numeric variable or field when you create it, so why not just write what size it is? In Rust the main numeric types look like this: i32, u64, u8. You just pick the one you want.
These typedefs as I'm used to them do address cross-platform issues.
Storage classes are "minutiae" however when all you want is just a straight up number.
Python gives me an Integer type when I want a whole number, or a Float when I want to represent partials.
I don't really care to be honest how that gets represented in memory in this case.
> Storage classes are "minutiae" however when all you want is just a straight up number.
You can have a single "straight up number" and mention the bit width in the language spec. The mere act of writing it down doesn't force coders to deal with any more minutiae than they already had to deal with.
> Yeah by way of higher-level abstractions ...
I strongly object to this. "float is at least x bits" and "float is exactly x bits" are the same level of abstraction, and almost every language, high or low level, picks one of those options.
Also you named Java as being on the easy side and that has four different integer sizes...
Double doesn't behave like a whole number.
java only has a single int type, which is 32-bit regardless of machine architecture.
I was suggesting double for your partials, not your whole numbers.
> java only has a single int type, which is 32-bit regardless of machine architecture.
I'm so confused.
You said having a "list of what width each type is" is bad because it forces the user to deal with "minutiae".
But that's exactly what Java does. int is 32 bits, short is 16, long is 64
And then you praise a type in Python that does the same thing as "double" in C. It's usually 64 bits, but it might be something else.
It becomes less important as we all converge on 64-bit machines with 32-bit "int" types, but it's still a monumental pain point for portability.
That doesn't seem like a good example. The data arriving over the network arrives length-prefixed, because 0 is a legal byte value for arbitrary data. What do you then gain by throwing away your existing knowledge of the length?
But other than that, excellent point about the wonkiness of special values.
If we're going to get pedantic ...
0 in this case is an offset. The number is what's behind it: The point at which water freezes (aka 273 °K).
In this case 0 indicates the absence of any offset.
Huh? The whole point of Kelvins is that their zero point is an actual 0 value. That's why Kelvins are a unit and °C aren't.
Q: An object's temperature is 20°C. The object's temperature increases by 10%. What is the new temperature of the object?
A: 49°C.
Could I ask that you update Wikipedia with your discovery? Sadly the page appears to be erroneously using the word "unit" all over the place! https://en.wikipedia.org/wiki/Celsius
Also, NIST might benefit from your guidance: https://physics.nist.gov/cuu/Units/kelvin.html
> The degree Celsius (°C) can refer to a specific temperature on the Celsius scale or a unit to indicate a difference between two temperatures
In other words, you can add a quantity of °C to a temperature value and you'll get another temperature value. But you can't measure a temperature in °C.
Compare how, for example, the python datetime library uses a datetime type and a timedelta type. A datetime plus a timedelta is a datetime. datetimes refer to points in time, and timedeltas don't.
°C measures a temperature delta, but not a temperature.
"As an SI derived unit", "or a unit to indicate", "the unit was called"
Three mentions of it being a unit in the first paragraph, alone. I understand the point being made; the conclusion that "celsius is not a unit" is bogus, however, by any common definition, including NIST's.
In a now-deleted comment, you linked to a Wikipedia page on Dimensional Analysis, which includes the sentence "to convert from units of Fahrenheit to units of Celsius".
As a mathematical entity it doesn't represent the absence of anything. It is just a symbol that has certain properties associated with it. There isn't a hole in the real number line where 0 should be: there is a number there.
It isn't pedantic to insist that 0 isn't a number, it is equivocal to do so. In most contexts it does not need to be treated like a special value. Temperature measured in degrees is an example where you don't need to treat 0 specially, at least not more than other values...
0 is neither. It's clearly a number which can be used in arithmetic and defines a specific integer quantity. (Unless you think 0 is identical to NaN...)
The problem is that in some languages it's used for either or both of the above logical definitions, when it shouldn't be used for either.
You may be thinking of NaN.
This is a very pedantic quibble, and I'm not even sure it's correct. ASCII has NUL as well, and ASCIIZ isn't a character set AFAIK.
> C++ NULL boost::optional, from Boost.Optional
First of all, nullptr, second, std::optional.
> Objective C nil, Nil, NULL, NSNull Maybe, from SVMaybe
Nil is not a thing in Objective-C, to my knowledge.
> Swift Optional
You're looking for nil. So it should be four stars?
> Swift’s UnsafePointer must be used with unsafeUnwrap or !
You're confusing Optional and UnsafePointer.
Also, Nil is absolutely a thing in Objective-C: it is a null pointer of type Class (whereas nil is a null pointer of type id; you should avoid mixing them up, though I will admit nothing much bad will happen as Class and id are generally co-polymorphic due to the type system being kind of lame. I am not sure they always have to be, though).
(And as someone who has been programming in C++ since before it was standardized at all, I frankly think listing NULL and boost::optional is totally acceptable and complaining about it as if C++11 is more canonical is just being annoying.)
Doing a quick search for how nil works in Swift, it apparently isn't a null pointer, so you are wrong there as well :(.
> nil means "no value" but is completely distinct in every other sense from Objective-C's nil.
> It is assignable only to optional variables. It works with both literals and structs (i.e. it works with stack-based items, not just heap-based items).
> Non-optional variables cannot be assigned nil even if they're classes (i.e. they live on the heap).
> So it's explicitly not a NULL pointer and not similar to one. It shares the name because it is intended to be used for the same semantic reason.
Given that I don't think any of the rest of your comment was legitimate criticism, I am frankly betting that your comment about UnsafePointer is also not useful, but I am kind of tired of having to analyze this comment at this point (I stepped in due to the note about character sets and the floor kept sinking).
I think ASCIIZ is the more common format, so I replied with all that in response to ASCIIZ being called "unusual". Most popular languages that actually allow NUL bytes in strings usually tend to support some encoding of Unicode anyways…
> Also, Nil is absolutely a thing in Objective-C: it is a null pointer of type Class
You got me…in my defense, I didn't know about this to my knowledge. It was still stupid of me to assume that DuckDuckGo would be case-sensitive when I searched that. I guess I should use this for Classes now instead of nil.
> I frankly think listing NULL and boost::optional is totally acceptable and complaining about it as if C++11 is more canonical is just being annoying.
One ships with C++, one doesn't; that's like saying Joda-Time is the canonical date library for Java instead of java.time. Although, I should probably ask you if you consider Joda-Time to be "more canonical" before listing it as an example…
> Doing a quick search for how nil works in Swift, it apparently isn't a null pointer, so you are wrong there as well :(. > I am frankly betting that your comment about UnsafePointer is also not useful
I should have been more explicit, since these kind of run together when you bring pointers into the mix, which I guess I should have realized once I read the footnote. I'm not really satisfied with the explanation given in the article, nor your rebuttal of my argument. Swift's nil is overloaded in a sense: for native Swift structures, it's the whole "Optional-as-an-enum" abstraction that we know about. For class types, and pointers, it's a bit more complicated: you just cannot assign nil to a UnsafePointer or a SomeClass unless it's "Optional", but the "Optional-ness" is completely in the type system and under the hood, in order to facilitate interoperatability with C, Objective-C et al. you need to actually have the type of the size be sizeof(void *), have zeroes in it, etc. You cannot actually set either of these types to nil if they are non-Optional unless you do illegal things. So when you set a "pointer" (being an Optional<SomeClass>, UnsafePointer?) here to nil, you are literally shoving a nil it in, which also happens to work well with Swift's type system and Optional.none abstraction because there is no way to subvert it legally. All of this was basically a long-winded way of saying that yes, Swift's nil is actually NULL, but the type system makes sure that you don't get a "bare NULL" which lets you pretend like the Optional enumeration abstraction works but under the hood, and semantically, it's the same thing as NULL.
Yes, this is why C-strings can't be used for ASCII, because C-strings can't contain NUL as a character.
> You're looking for nil. So it should be four stars?
nil isn't a null by the definition this article is using, because it's not a subtype of every reference type.
I can't speak for other IDEs, but Intellij will always warn me if I'm exposing myself to some NULL operations.
I do think better first class language support is the way to go though.
[0]: https://www.typescriptlang.org/docs/handbook/release-notes/t... [1]: https://kotlinlang.org/docs/reference/null-safety.html
I think it's a bit unlikely we'll fully get rid of null, but we can get rid of some of the pitfalls. TypeScript for example pretty much fixes the problem, by enforcing you check for null when needed, though TypeScript takes a handful of other soundness shortcuts. Go makes null less harmful by treating nil pointers like empty values by convention.
They allow for explicit NULL-ness (which is a necessary concept) without falling into the trap of making everything implicitly possibly NULL. And when NULL-ness is explicit you are then forced to explicitly handle it in order for your program to compile (in the case of Haskell and Rust).
Go's idea of nil, for example, seems OK to me, and the language would need to be way more complex to fix it. For example, it would need a type system with explicit nullness, or maybe even actual generics. But it mostly doesn't matter because doing things with nil doesn't crash nearly as much in Go. Like a nil slice just acts like an empty slice. You can even append to nil and it returns a non nil slice. You can call methods on nil. Etc.
The trouble with getting rid of nil to me is that it requires you to either have values at all times, or deal with the possibility that you don't at all times. Go has the very very nice property that you can initialize any type to a zero value and it should work as an "empty" object. Pointers without nullability don't have a zero value. Fixing nulls at the cost of getting rid of Go's properties for zero values would not be worth it.
But I think you're over-selling the zero values feature of Go. It is very rare to see third-party libraries that have zero values which do anything but cause a NPE when you try to use them -- mainly because they embed pointers and then you have the same implicit NULL-ness that causes NPEs everywhere. It is great that the core language managed to get zero values right in most cases, but it's far from being as wide-spread as you might hope.
Also (nil interface != nil pointer that fulfills interface) is a very common mistake I see in Go code, and while it's not necessarily related to the existence of a nil value it is still related to the general concept of nil in Go.
[And on the TypeScript comment -- you don't really need monads for Option<> or Maybe types. You just need algebraic types -- and TypeScript has those. Haskell does use Monads for Maybe, but that's because Haskell has many other type-theory things that make it necessary to support using Maybe as a monad.]
As I've said, Go does nil basically as well as you can without having algebraic types. But given the semi-anecdotal evidence that I've definitely seen my fair share of NPEs in production Go code in the past 5 years, it's clear that it's not sufficient.
Also, if the situation is really so bad.... How come no one cares? I saw no Go 2 proposal to fix this situation, just generics, less boilerplate for error handling. But nothing about nil pointers. It's not at the top of anyone's list. Generally, people feel comfortable with nil in Go in ways they don't in JS and C.
I will take that for fact even though certainly you disagree. So why do people feel more comfortable with nil in Go? Because nil is not an error.
In C and JS, there are cases where nil is treated as an error. For example, getElementById returns null when it finds nothing. If you designed this API in Go, you'd instead return nil, PLUS an error (or Perhaps, a bool, if no other possible errors exist.) You can argue semantics but in Go it's generally held that if you aren't returning what the user wanted you should return an error. Exceptions to that exist but probably mostly just string manipulation functions.
This convention is so strong, though, that it nearly eliminates nil pointer errors caused by edge cases. Most nil pointer errors you DO hit in Go are:
1. Set on nil map 2. Send on nil channel 3. Failing to check error
The thing is, if you are writing production code, and writing tests for your production code, this shouldn't even make it to your code repository. It's virtually a non issue.
Even better; in C and C++, when you hit a null pointer, you don't get an NPE. You get a segfault. Everything dies. Kaput. Go is obviously not alone in not having this issue, but it's surely worth noting that it doesn't.
Go is not going to die some day because it's "not safe enough" - it's absolutely safe enough to write reliable code. The difference is, writing reliable code in Go is easy because for the most part, all you have to do is follow conventions and unit test. Same cannot be said about C and JS where you will inevitably get blindsided by sharp edges.
If you are in a situation where you can't have runtime errors, it's a poor fit. I don't know personally any developers that are in this situation. If your code doesn't put lives on the line, its OK to have a runtime error. Most of your real outages will be due to other bugs, and probably more of then will be due to other people's bugs, natural disasters, human operator error, bad configuration pushes, etc.
If you really think Go NPEs are even a significant portion of Go reliability issues I'm gonna need more evidence.
Don't get me wrong, there are many other Go reliability issues. I just don't agree with hand-waving it away by saying that "you should have tested for it". I'm very in favour of testing (umoci is arguably the most rigorously-tested container image format tool, and it's written in Go) but I think that tests shouldn't be used as the solution for safety problems. The logical conclusion of such a view is that any language can be reasonably safe if you have enough tests -- and while this is true (just look at SQLite) it's hardly practical to replicate the degree of testing that SQLite does for every C project. In order for everyone to benefit, safety needs to be built in (and Go does have a lot of safety built in).
> Go is not going to die some day because it's "not safe enough"
You keep refuting claims I never made...
To this point, I bring up Go because Go is an example of a language where NPEs are basically a non-issue. There's not much to say there, if we still disagree on this point I'm not getting anywhere and I'm just going to give up.
Following that logic, though, it feels inappropriate that null pointers have this ridiculous stigma compared to other runtime errors. Would anyone care about Rust if it's only promise were to get rid of null pointer errors? I'd argue if rust still had NPEs but effectively solved concurrency issues it would be exactly as popular today.
I'm not claiming there's no value in alternatives to null. I am absolutely disputing the idea that null is the biggest mistake in computer science. Full stop, absolutely unconvinced. I can think of a lot of things I'd consider much worse.
I still prefer more runtime safety over less, but there is a balance to be had too.
It was fine to design a language with nullable pointers in the 70s. It's unacceptable nowadays. nil in Go is a major mistake.
It sort of does, because explicit nullability forces you to do many redundant null checks when you actually knew that something could not possibly be null.
that would need dynamically typed data...
Did we win? Did that make Go better? Fuck no. Most people aren't frequently hitting nil pointer errors in Go because unlike C the behavior is a lot more reasonable and the conventions a lot simpler. And by the way, we didn't fix all the runtime errors. Nil pointers are just one possible runtime error. How about out of bounds array access, memory exhaustion, race conditions?
And yeah, I get that you can also fix all of those things, which is then called Rust. But we don't need another Rust, Rust is a fine Rust. Go has, imo, much better ergonomics and most of the time it's just fine for what I'm doing. Like, writing small to medium size servers and utilities in Go has rarely been a regretful experience. And, even if we had no runtime errors we would still need unit testing to ensure our components are functioning correctly. So, most of the time I'm aware of when my code has runtime errors anyways.
Getting rid of null is not magic. It does not get rid of all runtime errors. And yes, it does impact ergonomics. I will take Go zero values at the cost of nil pointers, every day.
A zero value is much better than undefined value, I'll grant you that. I prefer the forced initialization approach (Haskell, presumably Rust and many others). If I add a new field, I want to know where I need to populate it. Or if you must, maybe a default value defined on the struct (perhaps that's also "considered harmful" for reasons I can't think of at the moment).
But it seems you prefer the ergonomics of default-zero. I don't get it, but I can't argue with preference.
By convention, you should design your code to also treat zero values as empty. In Go, the zero value of bytes.Buffer is a ready to use, empty buffer.
If you drop default zero, you lose a lot of convenience and gain a lot of ceremony. It's not the end of the world, but neither is the null pointer error. It's just another runtime error. Just like divide by zero.
https://news.ycombinator.com/item?id=10148972 (150 points | Aug 31, 2015 | 143 comments)
I have on a few occasions tried to write NULL-less code, and it adds a good bit of work.
- model all possible states for a value
- determine appropriate default actions for all types
- meaningful place-holder values
It's a good exercise, and I think more code should be written this way, but - as an Engineer I'm trying to model just enough of the problem to solve it. I'm not trying to simulate every possible outcome in that domain.
Certain corners of your problem simply don't need to be modeled, and what's more the effort needed to model them can just be too much.
NULL is a great way to just throw up your hands and go "I don't know and I don't care". Much as when modelling a physical system singularities typically represent phenomena that the model doesn't take account of, so it goes with NULL. It simply says "Don't Go There".
Haskell, for instance, has the 'fromJust :: Maybe A -> A' function that allows you to do just that. It unpacks the Maybe typed value and throws a runtime error if it fails.
Either you unwrap the Option, or you have to remember to do an manual null check. The second option is more verbose.
Sure, what's bad about it? A logic bug was detected, so the program should be terminated. Or how do you intend to continue?
Segfault is not so different from what happens if you do "fromJust Nothing" in Haskell or get a NullPointerException in Java. You can even write a handler for the segfault, but I guess that's rarely a good idea.
I intend to not have the logic bug in the first place, by encoding my invariants in the type system.
If you "know" that the value is present rather than absent, you must have a reason for knowing it, so explain that reason to the compiler. E.g. maybe you took the first element of a list value that you know is non-empty - so maybe you need to change the type of that value to a non-empty list type. That way the compiler can check your reasoning for you, and will catch the cases where you thought you "knew" but were actually wrong.
the way I program that is nothing but a pipe dream.
> If you "know" that the value is present rather than absent, you must have a reason for knowing it, so explain that reason to the compiler.
I might know that it exists for example because it is computed in a post-processing step after a first stage but before a second stage. So it exists in the second stage but not in the first. Relying on global data (which I won't give up) makes it practically impossible to encode that the data is not there in the first stage.
And that's not a problem at all. I simply don't access that data table in the first stage... Trying to explain my processing strategy to a compiler would amount to headaches and no benefits.
So the first stage could create a handle to it, or even just a phantom "witness" that you treat as proof that the value is present.
> And that's not a problem at all. I simply don't access that data table in the first stage... Trying to explain my processing strategy to a compiler would amount to headaches and no benefits.
Shrug. I found that errors would make it into production, because human vigilance is always fallible. And the level of testing that I needed to adopt to catch errors was a lot more effort than using a type system.
I don't think type systems help all that much. Type + instead of -, and you're out of luck.
Depends what conditions cause it; the hard part is being sure that every possible code path through the first stage will initialise the data, even the rare ones like cases where some things time out but not others.
> I don't think type systems help all that much. Type + instead of -, and you're out of luck.
Not my experience at all - what do you mean? If you declare a type as covariant instead of contravariant or vice versa, you'll almost certainly get errors when you come to use it.
2) Change a plus for a minus and it is still an int.
You can only use the global program order once, I'd rather save it to spend on something more important. val result1 = stage1(); val result2 = stage2(result1); ... means my code dependencies reflect my data dependencies and I'll never get confused about what needs what or comes before or after what (or the compiler will catch me if I do), so I can refactor fearlessly.
> 2) Change a plus for a minus and it is still an int.
True. If you get your core business logic wrong then types won't help you with that (though FWIW I'd argue that it's worth having a distinct type for natural numbers, in which case - and + return different types). But I found that at least 80% of production bugs weren't errors in core business logic but rather "silly little things": nulls, uncaught exceptions, transposed fields... and types catch those more cheaply cheaper than any alternative I've seen.
The situation you describe is one where a null really is an unrecoverable error, and the program should terminate. That is the one case where it makes sense to just let a NPE happen.
However, the vast majority of time, a null is just an absence of value, and does not signify an unrecoverable error. Those are the kind of situations that an Option/Maybe helps with, since it doesn't let you forget to handle the null case.
Even if a null value returned from a function is abnormal, and the program shouldn't continue, an Option is still going to be better most of the time. After all, you probably have connections and stuff you want to cleanly terminate before shutting the program down.
There's no reason to include sentinels that will randomly blow up your program.
So this "solid" distinction often is just noise and actually blurs the intention of the programmer: An explicit unwrap is required syntactically while it should not be required semantically because really the option data is not an option but a requirement in certain contexts.
In this fashion, you have type safety everywhere, and you deal with the case of a missing value in a predictible way, in a single spot.
Are you nuts? I prefer the compiler gives me an error instead of blowing up in production.
They are still NULLs however under the hood and I still need to do the work of defining what I want to happen when they occur. It's just neater.
I think if you read back over this thread it should be clear what I mean.
I proposed that NULL has a purpose. You proposed that Option obviates this. I stress that it's just a neater way to manage the conditions you don't need to model. You point out that in terms of implementation it's different, where I explain that still, logically it's the same thing.
Of course NULL has a very specific meaning in the structure of the language, and when you start using things like Options it makes managing NULL easier, but it's a rose by another name, gift-wrapped, and bundled with some plant food.
Logically however, at the point where you're modelling your problem it's the same.
Really depends on the language. On Java / C#, it's hard to avoid null-checking a lot of stuff. In C++ you will never encounter a null std::string outside of code written by indian students learning on Turbo C++ 3.5. So you don't need to check for anything: if it's a value, it exists.
Java's Optional seems fundamentally flawed in that Java allows any reference to hold a null value and Optional can still throw a NPE when calling isPresent on it so it still gives people a footgun.
https://www.youtube.com/watch?v=YR5WdGrpoug
https://dotty.epfl.ch/docs/reference/intersection-types.html
The alternative is to use data shapes that do not require something to be in a certain place. Hence the use of maps as the most basic data shape: you either have an entry in it, or you don't, no need to have a null entry. Expanding that thinking to databases, and you realise tables is not the right aggregate, instead you need to go one level down to something that datomic calls datom, or RDF calls a fact.
To summarise, PLOP forms that package together a set of slots to be filled magnify the issue of NULL/null/nil. Instead make the slot your primary unit of composition and make sure you use aggregation of slots that does not force you to have slots filled with a null value when there is no value in the first place.
Optionality doesn't fit in the type system / schema, because it's context dependent. For some functions, one subset of the data is needed, for others a different subset. Trying to mash it into the type system / schema is just fundamentally misguided.
The existence of Optional makes it possible to migrate away from using "null". E.g. Map#get could be replaced with a method that returns Optional.
Calling isPresent is the wrong way to use an Optional. But yes, it's an ordinary Java value, it can be null as long as Java-the-language permits null. That's not a problem with Optional, it's a problem with Java, and introducing Optional as a plain old normal Java class is the first step to fixing it.
There is a need for a nothing value in many cases and languages without an implicit null, such as Haskell, F# and Rust, employ an ‘option’ type. It’s a dedicated type that either contains a value or nothing. It forces you to declare when you expect a potential null and to check for it.
In numerical calculus: yes, most certainly. What do you expect the result of log(-1) to be? The alternative is to use specially tagged particular numbers as "no-data", and pray so that they do not appear naturally as the result of computations.
i pi, or more generally i pi n for odd n.
In short, the problem is not nulls per se, the problem is static type systems which does not allow you to specify if a certain value can be null or not.
But I hereby predict that soon people are going to declare that "nulls are bad" and eliminate them even in languages where they are totally fine.
Dynamically typed programs are usually informally "duck typed," since it's impossible to do something meaningful with truly arbitrary types most of the time. But just like in statically typed languages, there's always the very real possibility that your duck will instead be a null — and the bug is not the function returning a non-duck, it's you ever assuming you have something that quacks like a duck.
But of course you can make bugs involving nulls. The OP shows an example in a dynamic language where null is returned instead of the regular value if a lookup key is not found. Obviously this is ambiguous when the found value can also be null. But the problem is not the null per se, the problem is using a sentinel value to indicate a special condition when the same value is also a legitimate regular value. This is just bad API design.
[1] https://docs.scipy.org/doc/numpy/reference/maskedarray.gener...
None/null is for missing/unknown values (which may be a reasonable thing in your domain), while NaN is for the result of an illegal/undefined operation (which is definitely a bug, such as division by 0).
The problem isn't NULL, it's languages not enforcing the necessary checks for the "no data" condition. Option can still be NULL ("None" in rust), wrapping NULL in a struct doesn't provide any safety. The safety of Option wrapper types is from the other language features (like rust's "match") and a stricter compiler that forces the programmer to write the NULL check.
NULL would be fine if C required you to write this:
Obviously implementing that requirement would be difficult in C. Languages like Rust were designed with enforcement features (match + None, much stronger type/borrow checking), but lets your have "a value that is not a value".Talking about "NULL" pretty much implies that. When Tony Hoare talks about null references, it's about every reference being nullable in languages like Java or C#, not about the ability to conceptually wrap/opt non-nullable references in a nullability thingie.
if (foo_is_available) return foo; else throw FooNotAvailable();
By which I mean, in my highly sensitive and correct program xyz I can (say) abstract all access to a type (A struct?) through a macro which would contain null checking/assert-ing. This could work perfectly well and even be static checked for (in principle).
However, if I tried to package this type up in a library it all basically falls flat - or at least it would be very easily broken.
Doing this like this is to static analysis(/The Type system) as foreplay is to sex.
'None' in Rust is part of the Option enum, not an equivalent to null.
The advantage here (especially true in Haskell) is that you can use monadic error handling to make this far more pleasant.
1. it clearly separates "nullable" and "non-nullable" providing better modelling tools
2. the compiler assists/mandates null-checking, providing better type-safety
People say optionals are the solution, but the way I see it it's the other way around. Pointers types that allow NULL are basically optionals, and the problem is that we use them everywhere, even for things that are not optional. What we are missing are pointer types for things that are not optional.
And the pointer type with a guaranteed value needs to be as easy to use as the nullable pointer type, if not easier. Otherwise it won't always be used when it should be.
(Other note: C++ has a not_null pointer-like type: it's references. Unfortunately, C++ references cannot be reseated, which makes wholesale replacement of pointers not feasible. Plus, the language doesn't actually forces you to check pointers before assigning to a reference.)
https://elixir.bootlin.com/linux/latest/source/include/linux...
No, it doesn't totally replace NULL, but it does solve some of the problems in a high-performance way.
http://www.dbdebunk.com/2017/04/null-value-is-contradiction-...
Codd never proposed it in his original relational model. For good reason.
Furthermore you need to represent missing values somehow if you perform a left join.
However there are quite a lot of cases where it is semantically equivalent to "not applicable"; while some of these cases can be avoided with a restructure, some can't, and in either case I don't see a compelling argument to do so.
e.g. a parent_id column that is null for top-level objects in a hierarchy. Restructuring the database to avoid this seems like moving to an unintuitive paradigm in pursuit of rule-following for its own sake.
Data can be incomplete. Maybe only because it is not (yet) known. If NULL values were impossible it would create the need for one additional table with a foreign key relationship for every attribute that can be independently NULL. Sometimes this pattern is a good idea, but I don't think I can be convinced it should be the only possible solution.
Then make sure to have explicit defaults?
First of all, Codd's early work didn't propose it because Codd's propositions were idyllic and rooted in mathematics and not software engineering. In Codd's world, you would never create a FirstName/GivenName and a LastName/Surname field. You'd just create a Name field. The fact that you might want to sort by last name or the fact that someone named "Alice Taylor Smith" has a surname of "Smith" while "Robert Taylor Smith" has a surname of "Taylor Smith" isn't relevant. In Codd's world, you'd just define Name as a Name type, and the Name type itself would know everything about the entire domain of names. It would be about as complicated as working with a DATETIME. The same would be true for an Address type. Such a type would support both Western and Japanese addresses, which are wildly different. Codd doesn't need NULL because he has complex types which perfectly and comprehensively represent their domains. That's not realistic. Parsing Names and Addresses for even a single culture is notoriously difficult.
Second of all, Codd's later major work, The Relational Model for Database Management, includes his Twelve Rules (numbered 0 to 12) for database design in order for a DBMS to be considered relational[0]. Rule number 3 is:
> Rule 3: Systematic treatment of null values:
> Null values (distinct from the empty character string or a string of blank characters and distinct from zero or any other number) are supported in fully relational DBMS for representing missing information and inapplicable information in a systematic way, independent of data type.
So Codd clearly thought that nulls were essential.
[0]: https://en.wikipedia.org/wiki/Codd%27s_12_rules
NULL is just another case of a state of a variable. Other states are 1, 15, 0xffffffff, etc.
That mainstream languages don't handle this is the worst mistake of the computer industry.
It's simply a reality of how computers operate that when you allocate a piece of memory (a variable) it will have something in it that you'll need to clear or initialise.
In this respect, NULL is doing you a favour.
If you write your program with the "blow up early" mentality anway, or use static checking tools and a bit of discipline, I've found that null looses it's terror.
Additionally the program shouldn't "blow up early" but it was in many ways coded that way.
That very much depends on the language:
1. it can be a compile-time error (Swift)
2. it can be a runtime error (java, C#)
3. it can depend on what you're actually using (Python, Ruby, and on runtime extensions you might have loaded in the latter case)
4. it can be a no-op (objective-c)
5. it can depend on the combination of platform, compiler and surrounding code going from a segfault to deleting your program's security and/or causality (C, C++)
6. it can depend on the implementation and exact codepaths (Go)
etc...
any reference can be null, and calling a method on null produces a NullPointerException.
...which immediately tells you to go fix the code.
There are many times when it doesn’t make sense to have a null. Unfortunately, if the language permits anything to be null, well, anything can be null.
That's not an argument. See above.
3. NULL is a special-case
...because it indicates the absence of a value, which is a special case.
though it throws a NullPointerException when run.
...and the cause is obvious. I'm not even a regular Java user (and don't much like the language myself, but for other reasons) and I know the difference between the Boxed types and the regular ones.
NULL is difficult to debug
Seriously? A "nullpo crash" is one of the more trivial things to debug, because it's very distinctive and makes it easy to trace the value back (0 stands out; other addresses, not so much.) What's actually hard to debug? Extraneous null checks that silently cause failures elsewhere.
The proposed "solution" is straightforward, but if you reserve the special null value to indicate absence then you can make do with just one value instead of a pair, of which half the time half of the value is completely useless. If you can check for absence/null, you will have no problems using Maybe/Optional. If you can't, Maybe/Optional won't help you anyway --- because it's ultimately the same thing, using a value without checking for its absence.
Implicitly allowing all code to return nothing, and manually trying to remember what can return null and what can't, and checking that value, is incredibly error prone. It's really crazy that this has been the dominant way of handling the problem for many decades when their is a dead-simple way of ensuring it can't happen.
Null pointer errors, contrary to many claims, show up in production code all the time. Eliminating them is of huge value.
This is not the source of the vast majority of pointer errors.
Checking for (and trapping) NULL pointer dereferences is trivial, what is more difficult is the rest of the pointer range that doesn't get checked but is equally invalid, i.e. the other 4-billion (32-bit) possibilities.
Non-NULL-pointer checks are much more important than NULL checks.
The world of pointer issues is very much greater than "ASSERT(ptr!=NULL)".
...and as for correct error-recovery (not error-detection), well, don't get me started.
I think we write vastly different types of software. I can assure that that null-related errors are extremely common in situations besides resource issues. If it were just a resource-related problem, garbage-collected languages would almost never have issues, yet Java is infamous for NPEs. In Scala, where Options are ubiquitous, I've literally never had a single NPE.
It is very common for libraries to return null just to represent the absense of a result (ex: a row returned from a SQL query has no value for a column). That sort of thing means you have NPEs wholly unrelated to malloc or anything similar. These nulls are expected under normal program operation. They aren't errors. So, it's crazy to not to let the type system assist you in checking for nulls, so you don't forget and wind up with a NPE.
Pointer issues (that I was referring to) and a failure indication.
The most trivial pointer issue is a NULL pointer. This is such a trivial issue to catch its hardly even an error, yet people use that case as the exemplar for NULL issues.
detecting (and handling) failures on the other hand is very much different and more in the spirit of what the option-type arguments are about. In that case, the difficulty is not in detecting the error (that option-types will help with) but the application-level recovery. that is nothing that the language aid you with, its system-design and architecture related.
Basically, its the wrong issue to be thinking about.
How can you claim that NPEs are "hardly ever an error." NPEs are the most common error there is! They are indeed easy to catch, but you need to do so nearly everywhere, obscuring the code and introducing potential for error. There is no real, conceptual difference between something like a malloc returning null or a database query result containing a null. It is the same thing.
A null absolutely is an error if you don't catch it. By not using Options, it's vastly easier for that to happen.
not
"hardly ever an error".
in other words, NULL pointer errors are a trivial error to deal with.
[0]: https://en.wikipedia.org/wiki/Tagged_union#Examples
A) Assume value isn't null and have more readable code
B) Litter the code with null checks.
e.g.
becomes Thing with Maybe/Optional you have to check for presence of None, otherwise your code won't compile. Another smart way is what C# did. Integer can't be null. Integer? can be null.@MAYBE if(a.getStuff().getValue() == "TEST")
I think that you're being quite unkind. Haskell's Maybe type and Rust's Option types are very far from "leaky abstractions" and were developed by people who definitely understand how computers work. In fact, your description of them appears to indicate that you aren't really sure how they work (None doesn't take up "half of the value") -- the point of typeclasses is that cases where NULL is a reasonable value are explicit and your code won't compile if you don't handle NULL cases. Allowing NULL implicitly for many (if not all) types is where problems lie.
It also appears you're arguing that languages which don't have models that are strictly identical to the von Neumann architecture are "merely slathering everything [with] leaky abstraction". Would you argue that LISPs are just leaky abstractions?
https://doc.rust-lang.org/std/ptr/struct.NonNull.html
The article is not arguing that we shouldn't express or model the absence of a value. It is arguing that all types having to support "no-value" case leads to error prone code that has historically cost immeasurable amount of money and much credibility and respect. If everything can be null then it takes too much effort to null check every reference, so developers routinely forget or think they know better. Instead it argues that we should model the idea a possible empty values as a separate composable type. Then you can write a large percentage of your code with a guarantee that objects/type/values are never going to be nil, while still handling that possibility in the smaller error checking parts of your code base.
One interesting anecdote is that our team, working in Swift, had to integrate a crash reporting tool and verify that it works. The challenge was that we haven't seen a runtime crash in several months in production.
> A "nullpo crash" is one of the more trivial things to debug
If it happens in your debugging environment in front of your eyes then maybe. Some of us work on software that is used by millions over decades and would never get to see any reports from a majority of crashes.
Issues with null doesn't even register in comparison.
> That's not an argument. See above.
It is actually their best point, IMO. I really like how RDBMS/SQL solve this: fields hold values and you specify beforehand whether they can hold NULLs. The author is right, sometimes it does not make sense for variables to be null-able (think ID fields or usernames) but often it does (e.g. a user's avatar). Being able to indicate that would be a nice idea. C++ for example does that, as `Field x` is not nullable but `Field* x` is.
https://www.sqlite.org/testing.html
But which code? The point where you observe the error could be many compilation units away from the code that's broken; it might be in a separate project, or even 3rd-party code.
> ...because it indicates the absence of a value, which is a special case.
Why does it need to be a special case? Is your language incapable of modelling something as simple as "maybe the presence of this kind of value, or maybe absence" with plain old ordinary, userspace values?
> Seriously? A "nullpo crash" is one of the more trivial things to debug, because it's very distinctive and makes it easy to trace the value back (0 stands out; other addresses, not so much.) What's actually hard to debug?
"Tracing the value back" is decidedly nontrivial. And totally unnecessary if you just don't allow yourself to create that kind of value in the first place.
> if you reserve the special null value to indicate absence then you can make do with just one value instead of a pair, of which half the time half of the value is completely useless.
What do you mean? If you're talking semantically, you want absence to be a different kind of thing from a value: it should be treated differently. If you're talking about runtime representation, you can pack an Option into the same space as a known-nonzero type if you want to (Rust does this), but that's an implementation detail.
(Confusing sum types with some kind of pair seems to be a common problem for programmers who haven't used anything but C; sum types are a different kind of thing and it's well worth understanding them in their own right).
> If you can check for absence/null, you will have no problems using Maybe/Optional. If you can't, Maybe/Optional won't help you anyway --- because it's ultimately the same thing, using a value without checking for its absence.
Nonsense on multiple levels. Maybes deliberately don't provide an (idiomatic) way to use them without checking. By having a Maybe type for values that can legitimately be absent, you don't have to permit values that can't be absent to be absent, and therefore you don't have to check most values - rather you handle absence of values that can be absent (the very notion of "checking" comes from a C-oriented way of thinking and isn't the idiomatic way to use maybes/options) and don't need to consider absence for things that can't be absent.
The great advantage of Maybe/Optional systems is that only some of your references have to use them. You can draw a clear boundary between the parts of the code that have to check everything, and those that can prove it's already been checked.
In assembler we have no real type annotations, but for a long time I've considered trying to design a type-checking structure-orientated assembler.
But that's exactly the problem -- it's special, meaning it's only useful for certain situations. An ideal type system would provide compile-time guarantees, rather than having to wait for users to report issues. A type system which A) allows you to define variables as non-nullable and B) requires a null guard before every dereference of nullable variables eliminates this entire class of bug. What on earth is wrong with that?
EDIT:
> Seriously? A "nullpo crash" is one of the more trivial things to debug
Even if this were true [0], wouldn't it be easier if such a crash was just never even possible?
[0] which it's not - all a nullpo stack trace tells you is that something important didn't happen, at some point before this
Especially given that it can trivially be optimized out, since the eventual assembly should very well make use of the 0x0 property. But if you can encode the guarantee in your "high-level" language, why would you not want it?
In fact, I'm not sure how anyone could imagine assert(n != null) scattered throughout the codebase is a pleasant situation, unless of course, as most do, you're skipping the safety check for unsafe reasons.
So instead of calling methods on null objects the methods are just not called if the object is null.
This helps when there's a race condition, and you attempt to call a method on a null object, and then that solves itself by the same code being called again without the race condition.
But a lot of the time if the object is null and the method is not called you still have an error, but it's just not a null pointer error now.
This 'nullless' code is nice in some places, especially with UI lifecycles calling code repeatedly, but other times it just changes the type of error you debug.
It also provides backward compatibility with java where java types are considered nullable by default unless other wise annotated with a @NonNull. Also you get nice warnings about redundant null checks.
This provides for a lot of extra compile time safety and it largely removes the need for Maybe, Optional, and other kludges that people have been coming up with to force programmers to replace null checks with empty checks.
My own baby, Snigl [0], uses the type system to trap runaway missing values without wrapping. Which means that you get an error as soon as you pass a NULL, rather than way down the call stack when it's used.
https://gitlab.com/sifoo/snigl#types