Hacker News new | past | comments | ask | show | jobs | submit login

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.




> 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?


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.

https://doc.rust-lang.org/std/ptr/struct.NonNull.html


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.


`Option<T>` in Rust is basically identical to `T | null` in TypeScript.


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.


Not sure what world you are from. But here on earth apparently garbage collection and javascript is all the rage ;)

Issues with null doesn't even register in comparison.


> > 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.


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.


But why was the scenario not tested before production? Should that not be the case anyway?


You can never guarantee you really have 100% test coverage in all scenarios in complex software.


Indeed. It gets asymptotically more expensive. Whereas a typechecker is a system of tests that's able to "cover" 100% of the code.


It’s cute that you think tests find all problems.


   > ...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.


But expanded null checks could be automated by the compiler if so desired right? Without having to change the nature of null into an optional.

@MAYBE if(a.getStuff().getValue() == "TEST")


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.


> ...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.


Completely agree, this is CS theory gone off the deep end...


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.


Two things are getting conflated here.

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.


"hardly even an error"

not

"hardly ever an error".

in other words, NULL pointer errors are a trivial error to deal with.


They're even easier to deal with if your type system guarantees you can never get them in the first place.


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?


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.

[0]: https://en.wikipedia.org/wiki/Tagged_union#Examples


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.


I might have confused optionality and non-nullability...


Calling this "CS Theory" is intellectual dishonesty.


Option<&T>, in Rust, will compile down to a C pointer to T, with Nothing represented by the null pointer.


Also, when you are aiming for full test coverage null dereferences will be caught during testing.


Nobody does full path coverage, not even NASA.


sqlite is 100% branch test covered

https://www.sqlite.org/testing.html


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.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: