
Fighting the Borrow Checker - ingve
https://m-decoster.github.io//2017/01/16/fighting-borrowchk/
======
kaosjester
I really hate the phrase "fighting". Calling it a fight doesn't do justice to
the conversations you have with the borrow checker when you use Rust every
day. You don't _fight_ with the borrow checker, because there isn't a fight to
win. It's far more elegant, more precise. It's _fencing_ ; you _fence_ with
the borrow checker, with ripostes and parries and well-aimed thrusts. And
sometimes, you get to the end and you realize you lose anyway because the
thing you were trying to do was fundamentally _wrong_. And it's okay, because
it's just fencing, and you're a little wiser, a little better-honed, a little
more practiced for your next bout.

~~~
echelon
For a Rust practitioner, there is no fight against the borrow checker. It's
like saying a Java developer _fights_ the type system.

Is the borrow checker painful to grasp at first? A little. But the safety and
assurance it gives you is immeasurable. Write enough Rust, and before long,
you'll know when your code won't compile as written.

~~~
madez
Just because something is worth the hazzle doesn't mean people do it. Of
course, some people are willing to study and learn, but I think you also need
to cater to those who are not if you want widespread adoption.

~~~
ansible
As I understand it, the issues that people have with the borrow checker in
Rust are the same ones they'd have in another language like C++. Only there,
the language doesn't help you become aware of issues, like memory leaks or
other use-after-free type bugs.

If you really don't want a hassle, then maybe you want to program in a
garbage-collected language instead. But then you're potentially giving up some
performance and/or memory usage savings.

~~~
steveklabnik
To be clear, while this is true in the general case, there are specific
moments where it does break down, due to the conservative nature of static
analysis. This thread has some good examples, but in the interest of
transparency, I'll provide another:

    
    
        struct Foo {
            s1: String, // some sort of String data
            s2: &str, // a slice of said String
        }
    

What Rust doesn't understand here is that the String's data is on the heap,
and therefore, its address is stable. So if you try to construct this, Rust
won't let you, because it thinks that the borrow of s1 by s2 would be
invalidated by the move. This is fixed by [https://crates.io/crates/owning-
ref](https://crates.io/crates/owning-ref), but it'd be nice if it understood
it by default.

In general, these cases are small, but they can be annoying when you run into
them. As I said downthread, I very rarely run into them personally, but some
others seem to constantly. YMMV.

~~~
rrobukef
Is this actually safe?

What happens if you have ownership, then you can mutate the struct Foo like
this:

    
    
        foo.s1 = "World"
    

Which destroys the original heap data. Then s2 is a dangling reference. Since
references cannot implement a drop method, you need a wrapping datastructure
WITH reference counting, like you said.

~~~
steveklabnik
> then you can mutate the struct Foo like this:

You couldn't because it'd be immutably borrowed.

------
weberc2
I've often wondered why the extra scope is needed to borrow the same variable
twice in a function. It seems like the borrow checker is being overly pedantic
in this case by preventing users from writing a valid program. Rather than
training new users to pacify the borrow checker, perhaps the borrow checker
could be made to permit valid programs so as to reduce the learning curve?

~~~
steveklabnik
This is because the borrow checker is based on lexical scope at the moment. It
can (and almost assuredly will) be based on non-lexical scope in the future,
but enabling that has taken years of work to refactor the compiler in a way to
let it happen. We're close, but not there yet.

See also threads like this: [https://internals.rust-lang.org/t/accepting-
nested-method-ca...](https://internals.rust-lang.org/t/accepting-nested-
method-calls-with-an-mut-self-receiver/4588)

~~~
braveo
The borrow checker drawbacks was actually one of my biggest pet peeves with
Rust. If you guys can solve that problem in a meaningful way it'll go a long
way towards making me a lot more positive about using Rust.

As it was, I felt as though I were manipulating a child in order to get it to
do what I wanted.

~~~
Manishearth
FWIW a lot of the issues that newcomers come across are just due to lack of
familiarity. The feeling of manipulating a child is common initially, but soon
you'll learn how Rust expects you to design your code, and you'll be able to
write code that passes the borrow checker without much issue. This is true
when learning most languages; it takes some time to write code idiomatically,
and till then you'll be stumbling over many things.

But yeah, we should improve this.

~~~
vvanders
I'd concur, in fact I don't know if I'd like to see the borrow checker get
more complex because then it becomes harder to keep a mental model in your
head of what it's doing.

I found once I grasped the model that the borrow checker uses that I rarely
fight it and instead find it catching many things that I miss.

~~~
steveklabnik
Yes, I've said this as well. Non-lexical lifetimes will make the borrow
checker more complex; but if it tends to work closer to the way that you
intuit things, as a developer, it will _feel_ simpler.

Right now, the rules are very simple to explain. But that means your code gets
a little more complex at times.

~~~
Manishearth
I think a good example of this is how value categories work in C++ (even pre-
move-semantics).

It's initially quite intuitive and makes sense. But as you use the language
more you start questioning why it doesn't make sense and you eventually have
to learn how rvalues work.

------
nialv7
I really feel like I'm going lengths in order to satisfy the compiler. The
compiler should work for me, not the other way around.

Why do I need to manually create a scope when the compiler can see that
variable is never used again afterwards?

In Jill.name().first_name(). Why can't the compiler extend the lifetime of the
return value until the end of the statement?

Having a borrow check to ensure memory safety is good. But those are just
unnecessary and pointless. These are not pitfalls in user code, but pitfalls
in language design

~~~
steveklabnik
> when the compiler can see

If you look at the rest of this thread, you'll see that the compiler _can't_
currently see that; this is the whole issue.

~~~
nialv7
I wasn't talking about this particular compiler. I meant "in theory, a
compiler would be able to see".

~~~
steveklabnik
That's fair; as I said elsewhere, we are very interested in making your theory
a reality :)

------
lukewink
In the example given here, how would one answer the following questions:

1\. For a given school, return all the students

2\. For a given school, return all the classes

In the last data model given (the correct one), the above 2 things can't be
done (I think) without doing something funky like enumerating all the
Enrollments and finding unique students/classes. In order to modify the data
model to provide the needed information, wouldn't you run into the same borrow
checker problems?

------
alyandon
Why can't the rust compiler introduce a hidden temporary variable for the let
name = jill.name().first_name() example instead of requiring the developer to
explicitly declare and use one?

Edit: To clarify, I am a Rust neophyte and have only begun to scratch the
surface of the language.

Edit2: Also, "There is no reason a person should know which class they are in,
nor should a class know which people are enrolled in it. "

I know what the author is trying to say but when you read that at face value
it comes across as a little humorous.

~~~
Manishearth
> Why can't the rust compiler introduce a hidden temporary variable for the
> let name = jill.name().first_name() example instead of requiring the
> developer to explicitly declare and use one?

It could, but it doesn't yet. This kind of analysis needs porting borrowck to
MIR, which still has to happen. It's too complicated given the current way it
works.

Though it is a matter of explicit over implicit too. If `name()` had a
destructor you may want it to be explicitly tied to a scope by dint of being
declared in one instead of floating around in a magical hidden variable that's
tied to the scope of a different one. So there are reasons for why it may
never be made to work that way.

~~~
feanaro
> Though it is a matter of explicit over implicit too. If `name()` had a
> destructor you may want it to be explicitly tied to a scope by dint of being
> declared in one instead of floating around in a magical hidden variable
> that's tied to the scope of a different one. So there are reasons for why it
> may never be made to work that way.

I'm not sure I see what the problem is with this. It seems like it is very
intuitive behaviour in this case and exactly what you would expect.

~~~
Manishearth
Rust is more explicit about moving and scopes. It's currently very easy to see
how long a value will live and where the destructor will run. This somewhat
complicates that.

This is similar to vvanders' and steve's comments in
[https://news.ycombinator.com/item?id=13413084](https://news.ycombinator.com/item?id=13413084)
; such things lead to a more complicated mental model even if on the surface
it looks simpler.

