6 ms·
Non nullability is probably one of the most important concepts I've learned. People can talk your ear off about macros or about borrow checking or whatever cool
by _hardwaregeek 7y ago
Non nullability is probably one of the most important concepts I've learned. People can talk your ear off about macros or about borrow checking or whatever cool feature is in their favorite languages. But non nullability isn't a feature as much as the removal of a terrible one: types being automatically unifiable with null.
What does that mean? Well basically that every single (reference) type in most languages comes with an implicit "and null". String in Java? More like "String and null". Car object? Actually it's "Car and null".
Why is this a bad thing? Well null is a very specific type with a very specific meaning, but because it's automatically included with every type, people end up using it for a bunch of situations where a different type or value would work. Let's take a simple parser. A naive implementation, upon reaching the end of the string, might just return null. After all, nothing has been found. But that's not a null value, that's an EndOfString value! The moment you pass that value out of the context of the function, you need to remember that null means EndOfString. Or maybe the string you're passing in is a null value in the first place. It'd be tempting to return out null, right? Except you've now lost information on whether the string itself was null, or if something happened in the parse function that caused it to return null.
That isn't to say null is wholly evil. There's certainly uses for null. But it's often way better to contain its use with Option or Maybe, essentially wrappers that detail "hey, this value could be null". These wrappers are not unifiable with regular values, which forces you to think about where values can and cannot be null.
I totally understand if language designers want to omit features that they deem unnecessary or overcomplicated. I get it if you want a language sans generics or sans macros. But I don't understand keeping a feature that has caused far too much pain and encouraged far too many bad practices.
- WalterBright 7y agoIf you don't have a null, then you have to set aside some value for the type as the default value. With null, you get a seg fault if you try and use it. Without a null, you'll need to create a special default value which performs the same function - giving an error if you try to use it. Floating point values have NaN for this, UTF-8 code units have 0xFF. The D programming language uses NaN and 0xFF to default initialize these types. NaN is good because it is "sticky", meaning if a result is computed that depended on a NaN, the result is NaN as well. Some people complain about this, wanting floats to be 0.0 default initialized. But then it would be nearly impossible to tell if an unintended 0.0 crept into the calculation. > More like "String and null" Null's replacement, Maybe and Optional, still have the extra check.
- _hardwaregeek 7y agoNull certainly must exist at the target level. I'm not denying that. But languages can be designed to abstract nullability into controlled forms like Option/Maybe.
- XMPPwocky 7y ago> Null certainly must exist at the target level. Why? If anything, null must not exist at the target level- having a dedicated region of memory just for "accesses here are bad" at 0x0 isn't ubiquitous, and even having that at all isn't guaranteed (especially in embedded, where you might not get memory protection!). Modern operating systems have to keep userland from mapping the 0x0 page because it turns out programmers just cannot keep themselves from dereferencing NULL, and that's often exploitable when done from the kernel.
- sjolsen 7y ago>especially in embedded, where you might not get memory protection! Or where there's perfectly valid memory mapped to address 0
- WalterBright 7y agoThe 8086 put the DOS vector table at address 0, in retrospect a truly terrible choice. Any null pointer writes would trash the operating system. A much better design would have been to put the system ROMs at 0, where at least DOS would survive a null pointer.
- dbaupp 7y agoAnother way to address that problem is to not default initialise at all. There's then no need to worry about a default value for pointers, or about NaN vs. 0.0 for floats. In addition, null doesn't always give a segfault, as I'm sure you're aware. In C and C++, using (dereferencing) null is undefined behaviour, and a segfault is the best-case result. The code may have been optimised so that there's not a direct dereference (which would segfault) but instead other operations that rely on the pointer being non-null.
- wruza 7y agoI think we need more nulls then. In a sense. Consider a ‘cell’ (value box). 1. Null for no value there. But it can be set, that’s our conventional null. 2. Null for many values from a filter that cannot fit into a single cell. Can be set all at once. Not very null, but cell’s situation-wise it is. 3. Null for no value that cannot be set, e.g. filter produced no values to show or edit at all. 4. Null that is not equal to itself for relational joining in non-sql. (3=4? May be to some extent) 5. Null for a value that must be there, but is not yet (Promising Null). Cannot be set and throws on any comparison, since semantics are unclear until resolving. (4,5) require special isoftype() tests to check their type. 5a. Null for a value that is Promised only on access. Doesn’t throw, but instead yields until it’s done (or throws on failure). You can still check if it is already here, e.g. to choose a better access pattern. 6. Null for a missing key, as opposed to a key holding any of null values above. Absense of a cell itself. Not really 6, but another dimension (remember Perl exists $hash{key}, Js, Python ‘key’ in object, etc, that is hard to test from the object itself). I’m not a type scientist, but it is clear that semantics of a value in reality decompose in much more ways than just null/non-null. Objects (including primitives) should have at least so much null values when used not in a vacuum, but in loading cross-referenced dataset aggregations that appear in our programs everywhere. I’m not really making any counter-argument here, just my daily job observations to discuss! But I believe that a language that could cook it all right at the core level and remain convenient to program in will be a million years’ win.
- _hardwaregeek 7y agoWhich is why variants/discriminated unions are so powerful. A common "hump", if you will, that beginners get over with OCaml is an over reliance on booleans. A beginner will write a function that takes an isOpen argument. A more advanced programmer will model the state as a discriminated union that is either Open or Closed. Combine that with generics and you can get generic unions like Maybe which allow you to create sets of states that can be composed with any type.
- inopinatus 7y agoAlso from Ward Cunningham is the CHECKS Pattern Language (published as far back as 1994), which developed novel ideas about value objects well beyond the mainstream of the day. The examples are in Smalltalk but the uniform abstraction level for values is an instantly applicable concept, allowing generic treatment of value objects, and is probably more accessible to modern audiences comfortable with option types and so on than it was at the time. http://c2.com/ppr/checks.html http://c2.com/ppr/checks.html
- namelosw 7y agoI don't see a valid use case for null. It's just a easy-to-make billion dollar mistake. Null exist because people are being lazy, and don't want to think reference type T to be T | null. Instead, in most of the cases they ignore null unless they know it will crash. It seems to be every use case for null could be handled well in Haskell, which means null is not mandatory in language design. Everywhere using null can be replaced with Maybe or concrete union type like Int | NaN just fine.
- nerdponx 7y agoNo. The problem is conflating "logical null" (the absence of data) with "technical null" (a pointer to nothing). Logical null is often essential for expressing business logic in code. Technical null is (as far as I can tell) a C implementation detail that probably never should exist in a higher-level language. The Haskell equivalent of logical null is Nothing. There is no technical null because there is no need to represent that one specific implementation detail in Haskell. As for using NaN to represent logical null instead of a special reserved value, that has a lot of ergonomic problems. Anyone who has used Python for data analysis can attest.
- rogual 7y ago> The moment you pass that value out of the context of the function, you need to remember that null means EndOfString Good point, and I don't disagree, but it's also interesting to note that once you return 4 from CountChildren(), you similarly have to remember that it represents the number of children.
- _hardwaregeek 7y agoExcept you can always assign the result of CountChildren to a `childrenCount` variable. Is there a variable name you could give to the result of `parse` that'd accurately describe the error states?