4 ms·
So Rust has some cool safety features, especially for concurrent code. But, and perhaps I'm just uninformed, I never really understood the safety benefit of Rus
by filwit 11y ago
So Rust has some cool safety features, especially for concurrent code. But, and perhaps I'm just uninformed, I never really understood the safety benefit of Rust's 'never nil' design. Nil is a useful modelling tool, even in Rust where it exists via Option<>/None, correct? Perhaps by forcing you to be extremely explicit (and enforcing `match` always handles all conditions) you gain some arguable safety, but at what cost? It's certainly not easier to use and reason about, IMO. And it seems just as likely you'll end up crashing your program due to a bounds-check error (which may happen more often since Rust encourages indexing over references due to this very design.. at least, so I've read).
It seems to me the design was chosen more as a way to ensure memory lifetime could be better predicted by the compiler rather than any strong argument for safety.. but then, I'm not well read on the subject, and It's very likely there's good safety arguments for it I'm not aware of.. either way, in my experience nil-deref errors are rarely a painful thing. They happen often, but are also fixed quickly.
- steveklabnik 11y agoAs for null in general, you can hear it from the horse's mouth here: http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare http://www.infoq.com/presentations/Null-References-The-Billi... There are a number of ways to approach this topic, so I'll just give you one: in languages with more advanced static type systems, you try to encode as much semantic information as possible in the type. As you've said, the idea of null can be useful, so it deserves a place in the type system. You want to separate things that may be null from things that should never be null. This is because not-null is by far the common case. Allowing everything to be nullable by default optimizes for the lesser-used semantic, which is where errors with null come in: you assume that something isn't null, when it actually is.
- filwit 11y agoI agree the concept of 'non-nil' vars is very useful (and we have that in Nim), but I'm not entirely convinced by the rest of that argument. Namely, I don't agree that nil is rare enough to justify the verbosity Rust uses for it. Non-nil vars may be seen more often, but that doesn't mean nil vars aren't also often used, either. In Nim, both nil and non-nil vars are at roughly the same reach.. while in Rust non-nil vars are significantly easier work with. You may see that as a positive argument for Rust's safety (and you may be right for some domains), but I see it as more a negative argument for Rust's practicality.
- steveklabnik 11y agoFair enough. My argument here is more general than Rust itself, it's relevant to all languages with an Option type and no null. The verbosity can of course vary by language.
- pcwalton 11y ago> Namely, I don't agree that nil is rare enough to justify the verbosity Rust uses for it. Non-nil vars may be seen more often, but that doesn't mean nil vars aren't also often used, either. It's not verbose. "Option" is 6 characters. ".map" is 4. > In Nim, both nil and non-nil vars are at roughly the same reach.. while in Rust non-nil vars are significantly easier work with. Option values are really easy to work with. Just use map or unwrap if you don't care about handling the null case. If you do care about handling it (which you should, after all!) the code using "if let" is the same as the equivalent "if foo == null".
- filwit 11y ago> It's not verbose. "Option" is 6 characters. ".map" is 4. I just want to note that verbosity isn't just about symbol length, but also about operator noise and the number of available or required commands used to achieve a goal. Just counting these characters isn't very relevant, and isn't even the best Rust can do (as someone pointed out you can use 'Some()' to get an Option var, which is only 4 chars). That said, I agree this is rather subjective, and can't be well compared outside the context of the rest of the language.
- dragonwriter 11y ago> (as someone pointed out you can use 'Some()' to get an Option var, which is only 4 chars) Some() is 6 chars.
- filwit 11y agoI was measuring with pcwalton's ruler.
- 11y ago
- polyfractal 11y agoIt's not so much about "never nil", but rather "never accidentally null". Rust's compiler prevents you from moving data into a method which then nulls it out, leaving a dangling pointer in the calling code. At best this dangling pointer will look at garbage and cause a crash or undefined behavior. At worst, it will look at other, actively-used memory and cause a security vulnerability. Rust will just refuse to compile until this problem is fixed. Static analysis of these lifetimes allow a whole class of errors to be avoided (dangling pointers, double-free, iterator invalidation, etc). Rust's Options are handy for a lot of stuff (async APIs, concurrent code that may/may not succeed, error codes, etc). But they are just icing really, not really the main thrust of Rust's memory model. > It's certainly not easier to use and reason about, IMO. To offer a counter-viewpoint, I find that Option<> (and Result<>) are very easy to reason about. They tell you exactly what to expect from a function, and you don't have to guess if you need to catch exceptions or let them throw higher up. Everything is explicit, the only surprises are panics which are cataclysmic anyhow. > And it seems just as likely you'll end up crashing your program due to a bounds-check error (which may happen more often since Rust encourages indexing over references due to this very design.. at least, so I've read). If you use iterators in Rust, you never need to worry about out of bounds errors. In fact they skip range checking altogether, since you are guaranteed that the value you are iterating on won't change under your feet (no iterator invalidation, etc), which generates more efficient code[1] If you use explicit indexing, then yes, you can have a runtime panic if you go OOB. But that's the nature of explicit indexing. It also has to include those safety checks, so will be slower code. [1] https://doc.rust-lang.org/book/iterators.html https://doc.rust-lang.org/book/iterators.html
- filwit 11y ago> Rust's compiler prevents you from moving data into a method which then nulls it out Just for clarity, we have this in Nim too, eg: type Foo = ref object Bar = object val: Foo not nil let f = Bar() # Error, 'val' must be set proc foobar(f: Foo not nil): Foo not nil = return nil # Error, can't return nil foobar(nil) # Error, can't pass nil > At best this dangling pointer will look at garbage and cause a crash or undefined behavior. At worst, it will look at other, actively-used memory and cause a security vulnerability. In C/C++, yes, but this isn't so applicable to Nim where we have GCed references and 'not nil' constraints. > If you use iterators in Rust, you never need to worry about out of bounds errors Well I was not talking about iterating through a list, but rather maintaining arbitrary indexes to a mutable list. Eg, a Sprite which contains a index to a Texture array. In that scenario it's just as easy to miscalculate and crash your program via a bounds-checking error as it is to crash by nil-deref. > To offer a counter-viewpoint, I find that Option<> (and Result<>) are very easy to reason about.. It's good that Rust works for you, truly. And like I said in another post, I agree Rust's design here may be better for some domains. However, Nim's design still feels more elegant and straight-forward to me. Luckily, we both get a powerful language that suits us, regardless of which one we prefer :)
- pcwalton 11y ago> Nil is a useful modelling tool, even in Rust where it exists via Option<>/None, correct? It's not that null is not useful. It's that most pointers can never be null, so nullability is the wrong default. And it is useful for the compiler to force you to handle the case in which pointers are null. > Perhaps by forcing you to be extremely explicit (and enforcing `match` always handles all conditions) you gain some arguable safety, but at what cost? There's basically no downside to having no null pointers. With constructs like Option::map the code is usually even less verbose than the equivalent code with null. > It's certainly not easier to use and reason about, IMO. You never have to worry about your program failing whenever you type "." or "∗". With null, the semantics of the language are that an exception can be thrown [1] whenever those constructs are invoked. That's pretty much objectively easier to reason about. > It seems to me the design was chosen more as a way to ensure memory lifetime could be better predicted by the compiler rather than any strong argument for safety Huh? Lifetimes are totally independent. We could have had null pointers with the lifetime system (and there were languages like Cyclone that had both). The system exists precisely because of safety. We also get some really nice optimizations out of it that are impossible to get in C. All pointers in Rust are dereferenceable per the LLVM definition, which opens up some really neat optimizations like loop invariant code motion on loads. > in my experience nil-deref errors are rarely a painful thing. They happen often, but are also fixed quickly. Not in my experience. They show up in production all the time. [1]: Or you could do what Nim does, and make dereferencing null undefined behavior instead of guaranteeing that an exception is thrown, but that strikes me as worse than what Java does.
- filwit 11y ago> it is useful for the compiler to force you to handle the case in which pointers are null. Well I agree that it's very useful (and we have that in Nim), but.. > With constructs like Option::map the code is usually even less verbose than the equivalent code with null. I'm still not convinced of this part. It certainly hasn't been the case with the, admittedly small amount of, Rust code I've seen. However, I'll look for more comparisons in the future (or offer Nim comparisons to Rust snippets anyone posts). Point is, nil is still a useful and commonly used tool. So the argument for verbosity and conveniences is relevant, IMO. > With null, the semantics of the language are that an exception can be thrown [1] whenever those constructs are invoked. That's pretty much objectively easier to reason about. That completely depends on how often you want to use nil refs, and how easy they are to use. Like I said in another response, I agree Rust's design may be better for some domains, but I certainly wouldn't call it "objectively" easier to reason about in a general sense. > Huh? Lifetimes are totally independent. Well like my post implied, I was only guessing as to the design. And it's interesting to hear that it takes advantages of special compiler optimizations. That said, I still don't see how it's completely decoupled from the life-time system.. you're saying that if I have a Option<> reference to a mutable list in Rust, the compiler can determine weather or not the list is 'frozen' based on the runtime state of that reference? > Or you could do what Nim does, and make dereferencing null undefined behavior. I didn't think derefing nil was undefined behavior. I thought only dereferencing a pointer which points to once-valid-but-now-free memory was undefined behavior, and that situation is covered by GCed refs. Can you explain this a bit? EDIT: > Not in my experience. They show up in production all the time. I did say 'rarely', and I drew a comparison to bounds-check crashes, which surely also show up in production.
- Dewie3 11y ago> Nil is a useful modelling tool, even in Rust where it exists via Option<>/None, correct? Yes. So? A lot of things are useful modelling tools, but that doesn't mean you necessarily want to include them in every reference-like type. A tuple of two things is useful modelling tool; should that then be infused into every type? A "either value or error" is a useful type; should that be infused into every type? No? Then what makes "Either something or nothing" so special?