7 ms·
> Languages like Rust that don't even know null (or nil as in Go), have finally solved the problem. I'm in no way a Rust expert, but I see a lot of code with O
by rakoo 7y ago
> Languages like Rust that don't even know null (or nil as in Go), have finally solved the problem.
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.
- chii 7y agoUsing the `match` method, you can guarantee that in the `some()` branch, v is not null, and will never be null. 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.
- bcrosby95 7y agoIt solves it by making sure you handle it at compile time. 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.
- fidelramos 7y agoThe 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.
- reificator 7y agoNo that's not the key difference, because you can just call `unwrap()` and forget about it, which is effectively the same footgun as other languages. 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.
- crooked-v 7y agoTypescript is another one where null and undefined are unique singleton types, which are encompassed under the catch-all `any` type but, if strictness is turned on, are rejected by any other type matching. 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 undefined
- imtringued 7y ago> No that's not the key difference, because you can just call `unwrap()` and forget about it, which is effectively the same footgun as other languages. Are 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.
- reificator 7y agoPlease do not quote posts out of context in order to make your argument seem stronger. > 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.
- sgift 7y agoIf you don't care about the else case you can use if let: https://doc.rust-lang.org/rust-by-example/flow_control/if_let.html https://doc.rust-lang.org/rust-by-example/flow_control/if_le...
- hawk_ 7y agoThe difference is that the choice of Optional is deliberate in the parts of code where None is a valid value. In java you have no first class way to indicate that null isn't an expected value for some variable and so type checker can't block it. In rust type checker wont let None slip in accidentally if you don't use Optional.
- Sharlin 7y agoIn Java you're passed a reference to an object. Might it be null? Can it be null? Who knows! Mostly you're just going to ignore the possibility and hope for the best. 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>`.
- hcarvalhoalves 7y agoJust changed from having a bottom null type for all types to everything being a boxed Option type. It’s equivalent. The actual difference is the reliance on pattern matching and the compiler enforcing coverage on those languages.
- LessDmesg 7y agoOption types don't have to be boxed and are specifically optimized in Rust and, if I remember correctly, in Haskell. The Nothing is represented much like a null pointer, i.e. with a special value that can't be dereferenced.
- tome 7y agoNot in Haskell, FYI. Maybes are boxed.
- Sharlin 7y agoHuh, what? You don’t need or want most values to be potentially absent.
- thu2111 7y agoYes, the difference is a combination of types being non-null by default and compiler enforced checking. 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.
- tmpz22 7y agoI can't speak for Rust, but in Go accessing properties of nil objects and dealing with 0-values for non-pointer values can cause bugs in certain contexts where you "let your guard down". 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...
- steveklabnik 7y agoRust does not have nil objects nor zero values. The equivalent rust code would fail to compile.
- erik_seaberg 7y agoGo also has https://golang.org/doc/faq#nil_error https://golang.org/doc/faq#nil_error, so interface != nil doesn't ensure it's safe to call any of its methods.
- benibela 7y agoAnd you can write sth.unwrap() in Rust, and then it panics when it is null. Kotlin solves it much better. sth?.do_something() ?: nothing_to_do
- aw1621107 7y agoRust does in fact have a ? operator [0], but it works slightly differently than Kotlin's in that it returns early if a None/Result<Err> is encountered instead of just resolving to that None/Result<Err>. If that isn't what you want, then Option::map() is a fine substitute. [0]: https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/the-question-mark-operator-for-easier-error-handling.html https://doc.rust-lang.org/edition-guide/rust-2018/error-hand...
- benibela 7y agoBut there is still a function that panics. Java has solved memory safety. Now the task is to make sure that code that can panic will not compile
- aw1621107 7y agoThe function that can panic still exists, but the ? operator itself won't invoke a panic. Sort of like how you can still get NullPointerExceptions in Kotlin using !!, but standard practice is to use a safer option. 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.
- lmm 7y agoPanicing is the right thing to do: fail-fast is much better than silently continuing in an invalid state. If the None state is valid then don't call unwrap() (which you should almost never use, because if None was not a valid state then why were you using an Option in the first place?), call a function that lets you handle both cases, e.g. unwrap_or.
- steveklabnik 7y agoThis specific pattern can be re-written as sth.map(|v| do_something); incidentally.
- reificator 7y agoThat's a huge improvement. 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.
- imtringued 7y ago>It might look more pleasant, but it doesn't "solve" anything, only shifts it in a different place. 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.
- lmm 7y agoFor the case where something really could be absent, you're right; it's the same amount of code. The difference is that in a language with null you have to write that code for every single function parameter, whereas in Rust you can have required parameters that are actually required and only need to check the genuinely optional cases (which is what, maybe 5% of the time?). Make invalid states unrepresentable.
- pix64 7y agoOption<T> explicitly states the value may be none.