Upcoming changes to Rust's borrow checker
blog.rust-lang.org
blog.rust-lang.org
I've bumped into this kind of problem when writing rust, at first it was hard to understand why the compiler doesn't accept my code.
I understand it and consider it very reasonable choice given the goals. Still no fun ;)
Quite the opposite
But, still, the whole selling point of Rust is "flow-sensitive analysis (...) for a language feature that affects whether code is accepted or rejected". Accepting or rejecting some code based on whether it fits the memory model of that language is the main advantage that Rust has over other strongly typed statically compiled languages.
Is this something new here with this rendition of the borrow checker? I thought the whole premise of the borrow checker was to do a data flow analysis, decide whether to accept or reject the program, and if necessary to provide information about why the program was rejected.
It looks like what this project is trying to accomplish is to use better control flow mapping to reduce the number of unnecessary rejections that programmers see.
*excluding bugfixes, of course
What's actually hard to reason about is problem cases like in TFA which seem like they should be valid, but the compiler rejects anyway. These are cases where people's intuition doesn't match the current implementation, so I think moving the implementation closer to people's mental model of borrowing will make the language easier.
It turns out that programmers seem to think about scope in a cfg style, and don’t really think in lexical scope, even though it’s “easier.” Tons of people, even new ones, would look at some code Rust rejects and be like “but obviously this is safe because:” and then state the non-lexical reasoning. People generally find the non-lexical borrow checker to be far easier to use.
I think it is actually one of the larger things Rust has discovered, that doesn’t get talked about very much. And it’s also a great demonstration that simpler doesn’t always mean easier.
The entry point for people is almost always a reported error. The notion "live across a GC call" isn't natural to most people, so I dump out the entire live range (which might be from the previous iteration of a loop, for example) in the hopes that it'll sort of tell a story: "see, you set the variable's value to a pointer here, then you run this code, and this function call within it might GC, and then you use the variable's value here". But that's just an example path through the function, and potentially confusing if that particular path is not possible in practice (but another unmentioned path is). That's the risk of having a more precise analysis: there are more ways that the specific error can be wrong even though the report is a true positive.
Earlier, the analysis was simpler to mentally model: is there a path fragment that mentions variable X, then calls something that can GC, and finally mentions X again? But since then, I've added much more precise data flow to check whether the final use of X is an actual use and not clobbering the previous value (in which case, its live range ended sometime earlier). As with the borrow checker, that just provides reasons to not report an error that the simpler analysis would have, but those do matter to the user. Especially when they're struggling to restructure code, and having to mentally do the same analysis to figure out what changes will fix the problem.
In my experience, there are usually simple code fixes that eliminate a problem entirely and would have worked with the simpler analysis. For the more complicated cases, users are generally pretty appreciative when the analysis is smart enough to allow fixes to be more targeted and natural. (They aren't appreciative of the false positives that the more sophisticated analysis never showed them, because they pretty much only think of this stuff when they get an error report. But that's ok!)
- The cfg style checker accepts a strict superset of the programs that a lexical checker would (so if you're thinking lexically then your thinking will still work).
- The checker is (modulo bugs) sound, so if it thinks a program is valid then it is (so you don't actually have to think deeply about the complex cases if you don't want to).
it makes sense that this would be easier to use.
IMO Rust has already handicapped their language evolution with NLL and the more complex they make the checker the worse a pit they're going to dig themselves into. As long as it's simple enough to understand it's not too bad, but if we get to the point where people can't reason about the checker there'll be no coming back.
Do you see alternative paths from lexical lifetimes?
fn capitalize(data: &mut [char]) {
todo!()
}
fn foo() {
let mut data = vec!['a', 'b', 'c'];
capitalize(&mut data[..]);
data.push('d');
}
This code works just fine. But maybe we want to pull the slice out into a variable. So we do this: fn foo() {
let mut data = vec!['a', 'b', 'c'];
let slice = &mut data[..];
capitalize(slice);
data.push('d');
}
This code would fail to compile under the old borrow checker. Why? Because you have two &mut references pointing to the same thing. People are, I think, rightfully surprised that this code doesn't just work. It is very unintuitive that extracting a temporary to a variable causes code to no longer work. But that is exactly it: you are now changing the scope from small to large, thanks to lexical scoping.But people didn't talk about this code as like "Oh I understand that I am introducing a lexical scope and that means it conflicts." They talk about it like "but I don't use slice anywhere after the capitalize call. This code is safe. There's no point where these two mutable references are active at the same time. And you know I'm right because the temporary version works just fine, and this is equivalent." That is, their perception of scope maps closer to a CFG than to lexical rules.
Frankly, I am not even sure that many Rust programmers could describe what a control flow graph is, let alone that they somehow learn the exact rules and then apply them. People don't learn programming languages that way, just like they don't learn human languages that way. It is largely a practice of building up intuitions and testing them out.
I think a lot of programmers want to be able to understand how the language works in terms of formal rules and predict whether a given piece of code will pass the compiler without guess-and-check. Indeed, it seems like a pervasive complaint about Rust is that it requires too much of this “try things until it compiles” approach, and makes it hard to develop a clear mental model of how the borrow checker works.
If i understand correctly, the lexical scope idea is just saying that `slice` exists in scope until the end of the block regardless of what happens inside the block, whereas CFG is smarter and says that since we don't use slice after line 3, its not in scope anymore. So from my POV the non lexical scope is unequivocally better, more fine-grained reasoning from the type system.
Like:
fn borrow_field1(&mut self) -> &Foo { &mut self.field1 }
fn borrow_field2(&mut self) -> &Bar { &mut self.field2 }
fn do_something(&mut self) {
let field1 = self.borrow_field1();
let field2 = self.borrow_field2();
}I’ll use an Arc or an Rc depending on the need of thread safety. I think way too many people are over optimizing their Rust programs. Unless you are building for resource constrained devices, I think the cost of atomic primitives is negligible.
If you look around at lifetime recommendations, for a while they were so annoying that most people were saying “just force a copy and don’t use lifetimes”.
So often the advice is not "don't use lifetimes, because the borrow checker can't handle them", but rather "don't use lifetimes where an owned value is required" or "don't use lifetimes until you understand how ownership works".
But that doesn't help the compile-edit-compile cycle when you're working through borrow checking errors, so perhaps it's not that relevant.
There have been many similar changes in the compiler's lifetime, and so far additional type checking work hasn't caused noticeable slowdowns, apart from a few accidentally-quadratic bugs.
[0]: https://docs.rs/polonius-the-crab/latest/polonius_the_crab/i...
Declarative formulations of these problems are much easier to maintain, verify, and spec than a imperative (re)implementation.
Neither a borrower nor a lender be,
For loan oft loses both itself and friend,
And borrowing dulls the edge of husbandry.
He's a tragic but somewhat foolish figure, prone to fretting and offering kind of annoyingly obvious advice of which the above is an example.What he's saying is if you get accustomed to borrowing you will lose your ability to budget properly.
My friend loaned me $3000 cash when I was at the vet and my dog needed an urgent surgery. And I paid him back and he did not loose money and we did not loose friendship. And later on when I started to make good money I sometimes loaned to friends. One could not pay me back but we did not loose friendship either as he simply had circumstances beyond his control. I just told him consider it a gift.
We've given money to friends on multiple occasions to pay for medical bills, groceries, and the like, but every time I've entered a reciprocal business arrangement with friends or family (be that a loan or a paid work arrangement), I've regretted it immensely.
I have both loaned money and borrowed from, at various points in my life, friends and family. When lending, I have always looked at it as 1) A way to see how reliable my friend/family really was. 2) A sort of 'gift" - knowing full well they may not repay- thus I only loan what I am willing to lose. There have been times when I have lost more money than I would have liked to, but I knew it was a possibility going in, and thus I did not lose the friendship. I just know that at that point in time, the person I loaned to was not as reliable as I had hoped, and I know to be cautious in the future.
There have been many great business deals conducted between friends. If we decide not to do business with our friends because it is possible that the deal will go sour, we are left doing business with strangers, and we lose a lot of potential opportunities, not just to improve finances, but also to strengthen relationships.
You know all this talk about friendship, but you still mention "And I paid him back" in the same breath as "did not loose friendship". Sometimes you have to deal with the fact that you will never see the money again over such trivial things as a thoroughly mismanaged business venture.
The friendship was (and still is) worth a lot more than $2000 to me.
Later in his life he went to prison for a year (for unrelated reasons).
I think the positive experiences people are presenting are great. I don't think the Hamlet quote is remotely a hard-and-fast rule, but it does embody a principle that is worth stating, and which can be useful food for thought in making a lending decision.