Borrow self and innocent let expansion

Sorry for bait header. I do understand that Rust is imperative and let statement have side-effects.

I’m trying to understand how special self is after encountering an issue while trying my hands on Rust.

Consider trait

    fn try_take(&mut self, key: &str) -> Result<Self::Value, String>;
    fn empty_checked_for<T: Debug>(&self, val: T) -> Result<T, String>;

And now compare working code:

    let val = (h.try_take("a")?, h.try_take("b")?);
    h.empty_checked_for(val)

against one that results in borrow errors:

    h.empty_checked_for((h.try_take("a")?, h.try_take("b")?))

See playground also.

This code refactoring seems to be innocent. But in second case Rust decides to seize mutable reference for empty_checked_for method before computing argument. This quiet blows my mind as for the person who got taste of both C/C++ and Haskell.

Since Rust is imperative I’d expect stable order of argument calculation defined by language. And indeed fn empty_checked_for<T>(&self, val: T) can be considered as if self should be obtained before val.
But on the other hand self looses its special meaning for compiler if I try to write fn empty_checked_for<T>(val: T, &self) to force other arguments values (and release mutable borrows) before borrowing self.

Is there more Rust-idiomatic approach for similar code?

1 Like

This is a well-known limitation of Rust’s current borrow checker. I personally call it vec.push(vec.len()) problem, though other people may call it differently. Currently the borrowck takes source code as a syntax tree and process it basically statement-by-statement way. Polonius, a work-in-progress MIR-based new borrowck may fix this issue im the future.

2 Likes

@Hyeonu, that’s a pretty good minimal example you provided with vec.push(vec.len()). I was able to quickly find RFC 2025 for nested method calls. Thanks for that.

And thank you for a reference to an interesting problem. I read through non-lexical life-times introduction and double-checked with MIR-based borrowck post. Though it seems a bit different problem which is more about reducing scope (I hope) and thus lifetime. That seems to have some mitigation already in nightly build 2019-05-11.
As per merged pull request for two-phase borrowing I would expect that my problem should have been mitigated as well ~year ago. But changing playground build to nightly doesn’t fix build errors. Unfortunately I don’t have nightly Rust at hand to play with -Z borrowck=mir -Z nll -Z two-phase-borrows or something like that. What is even more weird that vec.push(vec.len()) actually works on stable.

Which leaves me quiet puzzled about what I did wrong…

Update: I guess I was able to get minimal example which boils down to a bit different problem vec.push(vec.pop().unwrap()) :rofl:, when argument expression requires mutable reference, but result is allowed to escape from reference and referenced object life-time.

P.S. This “two-phase” and “reserved” stuff sounds really dangerous for language consistency. I think it would be very hard to put in a words that people like me will understand this without diving into internals of compiler.
I think giving to first argument named self a special meaning that results in obtaining its value right before calling method but after building arguments makes more sense for me. Though there might be a still problem with non-method functions.

Two-phase borrows is just a term for what is going on internally, we won’t need to explain its inner workings in a similar way to how we don’t really have to explain nll too much to understand it. It will simply allow more correct programs to be written. Ideally, we shouldn’t have to think about it

That’s something that language define - “correct program”. Any program written in compliance with language specification should simply work or there should be an open bug on compiler/interpreter that fails to fulfill specification completely. And good specification should not require developers to look into it too often.

This life-time feature here is somewhat similar to constexpr in C++, requirements for which were relaxed from one version of standard to another.

Rust does not currently have a formal spec, there is currently work to make one (see Unsafe Code Working Group, and others like it).

But in the case of lifetime analysis, we can define a correct program as one that does not allow any pair of mutable references to alias. (The other checks are type-check problems, not lifetime analysis problems). So a correct lifetime analyzer, like lexical liftimes, nll, or polonius when it arrives (ignoring bugs), will enforce this property and the gold standard will accept every program that has this property.

While lifetime analysis will improve, the gold standard will not change.

Now this gold standard is unreachable (as solving it is reducible to the halting problem), but it is clear and precise. Lexical lifetimes, nll, and polonius provide a good sub-set of the gold standard that is easier to prove correct with static analysis.

This is similar to constexpr in C++, but with constexpr the gold standard is make everything executable at run-time, and as things are added the constexpr, those behaviors where added to the formal spec.


Note: what does it mean to alias. It means that if you have two references that you can write to that both reference the same memory, then those two pointers are aliasing each other.

Also notice how the gold standard in both cases are very simple, and easy to understand. Even if the implementations are complex.
In fact, it is because of this gold standard that RefCell is sound. If we didn’t have this gold standard, then RefCell could not be sound.

4 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.