I wouldn't quite phrase it that way, since it's nothing new. OCaml from the 1990s has no null value (you need to represent potentially missing values with Option). I suspect there are older examples than that.
Instead, with the implementation that we got in Java, if you tried to write this Map.getOption then if your map was ever used with old code that used nulls then it would break. So there's no way to ever start migrating to using options.
There are other problems with null, but they mostly boil down to the same issue: different code uses null to represent two different things, and so gets confused about what it means. (The other problem is that you can't look at a value and know that it can't be null, but optional definitely can't solve that until the whole ecosystem migrates to it).
If you ask me, they should have introduced another class, something like NullableOptional, similar to Optional for those use-cases. I don't agree that putting `null` in every optional just because of few bad APIs is a good idea, because people use Optional exactly to avoid dealing with nulls.
On the contrary, a selling point of Optionals is that they let you avoid that problem. Like I said, you could have a Map.getOption(key) function that unambiguously tells you whether the value is present or not, because it only ever returns None for absence, and will always return Some(value) (which might be Some(None) or Some(null), but those can be distinguished from None) if the value is present.
> I don't agree that putting `null` in every optional just because of few bad APIs is a good idea, because people use Optional exactly to avoid dealing with nulls.
Optional should be a transparent, consistent class that can contain any value that's valid in the language. Maybe Java will eventually reach a point where null can be considered invalid, but until that day, having Option ban putting null in it because it's old and deprecated makes about as much sense as if ArrayList banned you from making lists that contained deprecated classes.
Scala avoids the biggest pitfall of multiple inheritance elegantly, by only allowing one parent to have a constructor; classes may only be the first parent, and traits can't have constructors that take arguments. Unfortunately this doesn't mean they don't need to be initialized; in particular, vals in a mixed-in trait will be null if you try to access them in an earlier constructor.[2] As with any null, in the worst cases you may not get an error until much later on.
I'm in no way a Rust expert, but I see a lot of code with Optionals, something like
match sth {
Some(v) => do_something
None => nothing_to_do
}
This looks awfully a lot like if (something == null) {
nothing_to_do
} else {
do_something
}
It might look more pleasant, but it doesn't "solve" anything, only shifts it in a different place.Go is in the middle, because structs have no null value, only a zero value; the only thing that can be nil are pointers which I personally feel should used as little as possible.
In Java you tend not to know where or when null checks have or will happen. So you tend to sprinkle null checks all over your code just incase someone somewhere forgot to check.
The key difference is that now the type system can tell you the truth. In other languages when you have a reference to a type T, null is considered a valid T but you cannot treat it as one or everything blows up. Meanwhile in rust when you have a reference to T, you don't have a secret (not)T that invisibly breaks everything.
doThingA(param: any) // you can pass anything including null and undefined
doThingB(param: object | number | string | boolean | bigint | symbol) // you can pass anything except null and undefinedAre you sure? Each line of Java or Javascript can be equivalent to multiple `unwrap()` calls. You can't see them because the compiler is generating ([0]) them but they are there and there are many hundreds of thousands (or maybe even millions including dependencies) of them in most Java projects.
Here is an example: company.board.members.size() is equivalent to company.unwrap("NPE).board.unwrap("NPE).members.unwrap("NPE).size() and every single line in Java is like that. You can't opt out. You can't tell your coworkers to stop using "unwrap" during code reviews. The entire language forces you to do this in every single line that involves a reference.
[0] Well, it's actually just trapping the 0 address but it's equivalent.
> Please respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize. Assume good faith.
I am well aware of the issues in other popular languages. You and I agree that there is an implicit `unwrap()` call in each dereference operation.
The post I was responding to claimed that in Rust you cannot leave null/None values unhandled. This is false because `unwrap()` exists and more importantly is in fairly common use.
The distinction is not whether you can (at your own peril of course) assume a value is not null. Rust lets you do that.
As you point out however, it does force you to be more explicit. Why is that? It's because the concept of `None` is not hidden from the type system.
The reason that your Java examples work that way is not because `unwrap()` is implicit, it's because the type system doesn't know about nulls in the first place.
> The key difference is that in your second example you can just forget about the else branch, while in Rust you must explicitly handle the None case, otherwise it won't compile.
Do you really think that `unwrap()` is meaningfully different from "forget[ting] about the else branch"? Does it require you to "explicitly handle the None case"?
My goal here was not to get mired in semantic arguments that ignore the context of the discussion. It was merely to highlight that Rust lets you assume that things won't be null the same as any other language, and that the important difference is that rust lets you be clear about what can and cannot be null.
I think this is the point of contention. Languages with some Option type and no null are not the same as any other language. Yes, you can ignore error handling, but you cannot forget it.
I believe that claims that Rust forces you to not "forget about the else branch" are actively harmful to language adoption, I've seen many people use `unwrap()`'s existence as evidence that Rust's approach to empty value types doesn't actually provide any useful benefits.
If you're implying that coders use unwrap() pre-emptively, I don't think there's any evidence for that. Besides, you can't just slap it on every value you have - it must actually be a result type! - so even if it's applied incorrectly in advance, that still requires some conscious consideration.
This is not at all the same as forgetting an "else" - or, far more often, forgetting the "if" altogether.
match x {
Some(v) => return v
None => die painfully
}
?If so, the None is still handled. Dying painfully is an escape hatch out of the type system in any language that lets you do it.
> The key difference is that in your second example you can just forget about the else branch, while in Rust you must explicitly handle the None case, otherwise it won't compile.
Do you really think that `unwrap()` is meaningfully different from "forget[ting] about the else branch"? Does it require you to "explicitly handle the None case"?
My goal here was not to get mired in semantic arguments that ignore the context of the discussion. It was merely to highlight that Rust lets you assume that things won't be null the same as any other language, and that the important difference is that rust lets you be clear about what can and cannot be null.
I'm not sure I agree with this though:
> Do you really think that `unwrap()` is meaningfully different from "forget[ting] about the else branch"? Does it require you to "explicitly handle the None case"?
I haven't used Rust, so but digging up the source shows unwrap is indeed just a `match` that panics on None.
The fact that "you can put thing inside a function and call that function" doesn't mean you "don't have to explicitly do thing". We end up with a pretty useless standard for "explicitly doing thing" if putting thing in a function doesn't count.
In Rust you're passed a reference to an object. Can it be null? No, it simply can't! There does not exist a magic "null" value for references. To represent the possible absence of a value, you explicitly encode it into the type system by using `Option<T>`.
The actual difference is the reliance on pattern matching and the compiler enforcing coverage on those languages.
This doesn't require an option type. Option types are actually quite awkward compared to language integration. Kotlin doesn't use an Option type yet still delivers all the same benefits as Rust/Haskell's approach, with benefits (e.g. zero overhead, integrated syntax). Note that Option type overhead isn't merely about heavyweight syntax and the need for the compiler to successfully scalarise: using language integration around standard null pointers means the CPU spends no time on checking the 'maybe' branch. If the type system wasn't violated and a value is present, it's used immediately without any additional instructions or code bloat. If it's not then that will trigger a page fault at some point in the CPU pipeline and transfer control to an exception handler. If the type system indicates nullability and you need to actually do more than just unwrap it, then you have to take a branch of course but it's fully inlined and cheap.
I agree. In Java at least the semantics are practically identical.
I can't comment on Rust. I suspect it does a slightly better job.
Crystal is another language where there is no null. It's quite cool.
Java and Rust are practically the same regarding the differences between Optional<T> and T (java), and Option<T> and T (rust). Optional<String> in Java has the `.get()` method which corresponds to the `.unwrap()` method in Rust. And in both are different different types than String in their respective languages.
But in Java you can have an Optional<String> that's null, not just empty. So that's three states - null, wrapping null, or wrapping a value.
I'd say the big difference is that in Rust you can have places that refuse nulls (e.g. using T instead of Option<T>), so you have well defined points of control/checking. That, combined with the difficulty in recovering from a panic (the equivalent of a null pointer exception when trying to unwrap a value that's missing) and the Result type, are Rusts real strength wrt. missing values.
Regarding unwrap: If you tell the compiler "I know what I'm doing" and you don't know what you are doing then no one can help you. It's the same as putting casts everywhere when the compiler yells at you about incompatible types: If you lie to the compiler it will do your bidding, but your code will run into errors at runtime that you could have prevented at compile time. Again: No one can help you here.
func getFoo() (*Foo, error)
func main(){
foo, err := getFoo()
if err != nil {
// be an adult and handle your error
}
foo.Work()
// panic because whoever implemented getFoo returned a nil object
}
Also worth noting dealing with booleans in JSON is wonky because boolean values default to false, even if that json property wasn't set over the wire. Thus you end up unmarshaling to a pointer to a boolean, which now requires nil guards throughout your codebase when working with that unmarshaled object...The equivalent rust code would fail to compile.
Kotlin solves it much better. sth?.do_something() ?: nothing_to_do
[0]: https://doc.rust-lang.org/edition-guide/rust-2018/error-hand...
Java has solved memory safety. Now the task is to make sure that code that can panic will not compile
Exception/panic-free code is fairly straightforward; you just need to have APIs return Option<_>/Result<_>/other error code equivalents to indicate failure. The problem with that is that it introduces some amount of friction that will probably make some programmers unhappy.
If you want to avoid returning Option<_>/Result<_>/other error code equivalents, you need some way to prove your inputs cannot trigger an invalid result, and that is a very difficult problem.
With this new feature I'd argue the Java/Kotlin world now has the best handling of optionality of any language, anywhere:
• Pleasant, concise syntax for handling optionality. Not bolted on with an option type.
• Highly efficient translation to machine code. No boxing, no unnecessary branching in the unwrap/!! case.
• Excellent interop with older code written in OOP languages that don't represent nulls in the type system.
• But that older code can be annotated with @NotNull and @Nullable to retrofit the information; within Java IntelliJ will statically analyse these and add warnings where an NPE/panic would occur, when such code is used by Kotlin the annotations cause the types to be correctly derived as nullable or non-null.
• The large wealth of libraries that want to explore object graphs continue to work without being distracted by option/maybe types (e.g. serialisation, UI binding)
• On the rare occasions where you do need to unwrap and you got it wrong, you now get an error message that breaks down the sub-expression where the null pointer occurred, so there's no incentive to break up long chains of dereferences just to get better debugging in case of failure.
That's a featureset around optionality that's hard to match.
> • Pleasant, concise syntax for handling optionality. Not bolted on with an option type.
Optionality is not special enough to be worth a special case in the type system, IMO. Maybe some syntactic sugar could be worthwhile, but not a magic type that behaves differently from other types (which is what Kotlin's ? is) - you need it to behave like a normal type so that you can write and reuse generic code. Arrow-kt can't implement functions that work with nullable values, and has to implement its own Option type instead.
> • Highly efficient translation to machine code. No boxing, no unnecessary branching in the unwrap/!! case.
Getting the semantics right is the important thing; it doesn't matter how fast the code runs if it's broken. It's still possible to compile a well-behaved option type into something unboxed in the cases where it can be represented that way (look at what Rust does, where the option is "packed" if 0 is not a valid value for the inner type, but still does the right thing for Option<Option<T>>, Option<Int> and so on).
In the `if == null` method, the `something` reference is null, but if you have functions that also reference `something` down the line, you're going to have to check it again for null-ness.
sth.map(|v| do_something);
incidentally.It means that you've told the type system what it needs to support you.
If your function tells the type system you need a reference to a type, you get a reference to that type. There's no hidden timebomb in there where you might get something that supposedly is that type but can't be treated as that type.
This lets you write your functions to accept nulls where appropriate, and require some form of unwrapping at the call site otherwise. It tells you when you've missed something and it lets you refactor without having to constantly repeat all of your checks, once you know something is valid the type system reflects that for you.
In short if you have a T in rust, you know that it's not secretly a (not)T that is implicitly allowed anywhere a T can be.
It shifts it right into your face. You're forced to do something instead of pretending that everything could be potentially null. Since the only places where you can actually encounter "null" are documented you are no longer wasting your brain cells on the happy path where you are guaranteed to never see a null value when you don't expect it. Instead you can free up all that cognitive capacity to actually write correct code that handles null values.