5 ms·
> There are great many ways to solve them. The most common ones are 'null' and 'Optional[T]'. Neither just makes the problem magically go away. If a process is
by kerkeslager 7y ago
> There are great many ways to solve them. The most common ones are 'null' and 'Optional[T]'. Neither just makes the problem magically go away. If a process is designed (or a programmer writes it) thinking that 'ah, well, here, not-a-value cannot happen', but it can, then.. you have a bug.
> Some language features might make it possible to help reduce how often it occurs, but eliminate it? I don't think so.
On the contrary, you can 100% eliminate it by forcing null handling at compile time with your `Optional` type. Haskell and some other strongly-typed languages do this (but they call it Maybe).
The way to do this in a C-syntax-ish language would look something like this:
Optional<int> increment(Optional<int> i) {
// return i + 1; would throw an error at compile time,
// because Optional doesn't implement the + operator
// i.applyToValue would throw an error at compile time
// if you didn't handle both possible cases
return i.applyToValue(
ifNull: (void) => { return new Optional<int>(null); },
ifValue: (int i) => { return i + 1; }
);
}
This is syntactically a bit heavy, partly because I was a bit more verbose than a real implementation would need to be, for clarity, and partly because C-style syntax doesn't do this well. Languages that support this generally have some syntactic sugar to make it a bit more terse.
I've argued before on HN that the benefits of strong static typing are overstated, but this is a case where strong static types really do completely eliminate an entire category of errors. Given how common these errors are, not using stronger types in this situation for popular languages has absolutely been a billion dollar mistake.
- masklinn 7y ago> Haskell and some other strongly-typed languages do this (but they call it Maybe). Most call it either Option or Optional FWIW. `Maybe` is the term used by Haskell and its derivatives (like Idris or Elm).
- tsimionescu 7y agoThe problem GP was raising is on the other half of Optional: when you do finally need an int. Say, you want to do arr[*increment(maybeInt)] //error: can't dereference Optional<int> Now, if you as a programmer don't think increment(maybeInt) can actually return None in your particular case, you will probably do the minimal work to convince the compiler to let it go, say matchOptional( increment(maybeInt), someInt => {return arr[someInt]}, () => { /*never happens*/ return 0; }) (using a slightly simpler notation that your version, since I'm on mobile) Now, if you were wrong and you do get a Nothing, instead of seeing a nice stack trace, you have an absurd 0 running forward. You could improve this using an assert() but this is what managed languages already do with the NullPointerException&friends. The more interesting thing to show is all the code that does not deal with optional and that is now magically free of any possibility of null errors. But the programmer is still responsible for correctly treating the moment they need to go from optional values to non-optional, and here Optional is essentially just more in-your-face than null (which is valuable, don't get me wrong). The only example I know of where a language feature truly completely eliminates a category of errors is managed memory, which does not replace memory errors with any other more-or-less equivalent error case. I personally very much doubt NULL is a significant source of errors in managed memory languages. It's not nothing, but they are some of the easiest bugs to track down.
- kerkeslager 7y agoIf it really can't ever be null, then it should just be an int, not an Optional<int>. The entire reason that it is an Optional<int> is that it CAN be null. In this hypothetical language, not initializing an int is a compiler error, assigning null to an int is a compiler error, etc. If it's an int it literally cannot be null. What ends up happening in practice is that the null is handled close to where it's created, and the rest of the code passes around an int you know isn't null, because people don't feel like passing around an Optional<int> and being forced to check it everywhere. Sure, you can intentionally write code that does the wrong thing in any language, but the "return 0;" would be a very obvious error in even cursory code review.
- tsimionescu 7y agoThere are normal cases where this can happen. For example, a map should normally return an Optional<ValueType> when you try to retrieve a key's association. However, there may be special cases where you know that a key is present (maybe it is a constant map, maybe you just set the value of that key etc). I do agree that these cases are much rarer than the cases where a value is either always there, or the cases where a value really can be missing. I was only pointing out that Optional doesn't eliminate 100% of null errors, just 99.9% of them.
- kerkeslager 7y ago> For example, a map should normally return an Optional<ValueType> when you try to retrieve a key's association. However, there may be special cases where you know that a key is present (maybe it is a constant map, maybe you just set the value of that key etc). Both of these cases are still pretty big code smells. 1. Just don't use constant maps. Instead of doing this: const myConfig = new Map<string, int> { "height": 72, "weight": 160, }; [...] var height = myConfig["height"].matchOptional( ifNull: () => 0, ifValue: i => i, ); ...use a constant structure (with an anonymous type): const myConfig = struct { height: 72, weight: 160 }; var weight = myConfig.weight; You can verify whether height or weight are null at compile time this way[1]. 2. If you just set the value of the key, instead of doing this: dictionary[word] = getDefinition(); let definition = dictionary[word].matchOptional( ifNull: () => "", ifValue: d => d, ); ...do this: let definition = getDefinition(); dictionary[word] = definition; I'm aware that I'm playing fast and loose with the syntax of our pseudo-language, but note that avoiding the optional will is terser and simpler than using the optional and eating the null case--this is true in most cases in most strongly/statically-typed languages. Not only do you learn to lean on the type system in a strongly/statically-typed language, but if the syntax is well-designed, it makes it easier to lean on the type system than to not lean on the type system. [1] You may say, but what if I'm loading from a file? The common pattern is to load a config from a file as a map, and then load it into a struct, setting defaults, like so: const defaultHeight = 72; const defaultWeight = 160; JsonObject configJson = json.loadFile("config.json"); const config = struct { height: configJson["height"].matchOptional( ifNull: defaultHeight, ifValue: v => v.asInt(notInt: v => throw Exception("Invalid height \"{}\" in config.".format(v))) ), weight: configJson["weight"].matchOptional( ifNull: defaultWeight, ifValue: v => v.asInt(notInt: v => throw Exception("Invalid weight \"{}\" in config.".format(v))) }; You eventually hit cases with user data where you can't handle it (hence the throwing exceptions) but this pattern allows you to fail early, and with descriptive error messages.