I am trying to code a "cached" function: i have some container (called Foo in the example below) and would like to get a reference to its content if any, else compute that value and store it before returning a reference to it.
In the examples below, bad and bad_unsugared fail and I have an idea as of why.
However, bar does work (instead of returning a reference I use the value right away), but baz does not.
I keep trying to understand how I could be in violation of Rust borrowing rules on runtime but I can't find the problem.
Can you help me understand it? Thanks.
trait Foo {
fn get<'a> (self: &'a Self) -> Option<&'a str>;
fn insert (self: &mut Self, _: String);
}
fn bar (foo: &mut Foo) // works !
{
if let Some(s_ref) = foo.get() {
return println!("{}", s_ref)
}
foo.insert(42.to_string());
bar(foo)
}
fn bad (foo: &mut Foo) // error
{
if let Some(s_ref) = foo.get() {
println!("{}", s_ref)
} else {
// idea: None: Option<&'borrow_lt str> here explains the error
foo.insert(42.to_string());
bad(foo)
}
}
fn bad_unsugared (foo: &mut Foo) // error
{
match foo.get() {
Some(s_ref) => println!("{}", s_ref),
_ => { // idea: None: Option<&'borrow_lt str> here explains the error
foo.insert(42.to_string());
bad_unsugared(foo)
},
}
}
fn baz (foo: &mut Foo) -> &str // error??? why?
{
if let Some(s_ref) = foo.get() {
return s_ref
}
// idea ??
foo.insert(42.to_string());
foo.get().unwrap()
}
fn main () {}
Compiling playground v0.0.1 (file:///playground)
error[E0502]: cannot borrow `*foo` as mutable because it is also borrowed as immutable
--> src/main.rs:21:5
|
17 | if let Some(s_ref) = foo.get() {
| --- immutable borrow occurs here
...
21 | foo.insert(42.to_string());
| ^^^ mutable borrow occurs here
22 | bad(foo)
23 | }
| - immutable borrow ends here
error[E0502]: cannot borrow `*foo` as mutable because it is also borrowed as immutable
--> src/main.rs:22:9
|
17 | if let Some(s_ref) = foo.get() {
| --- immutable borrow occurs here
...
22 | bad(foo)
| ^^^ mutable borrow occurs here
23 | }
| - immutable borrow ends here
error[E0502]: cannot borrow `*foo` as mutable because it is also borrowed as immutable
--> src/main.rs:31:7
|
28 | match foo.get() {
| --- immutable borrow occurs here
...
31 | foo.insert(42.to_string());
| ^^^ mutable borrow occurs here
...
34 | }
| - immutable borrow ends here
error[E0502]: cannot borrow `*foo` as mutable because it is also borrowed as immutable
--> src/main.rs:32:21
|
28 | match foo.get() {
| --- immutable borrow occurs here
...
32 | bad_unsugared(foo)
| ^^^ mutable borrow occurs here
33 | },
34 | }
| - immutable borrow ends here
error[E0502]: cannot borrow `*foo` as mutable because it is also borrowed as immutable
--> src/main.rs:43:3
|
39 | if let Some(s_ref) = foo.get() {
| --- immutable borrow occurs here
...
43 | foo.insert(42.to_string());
| ^^^ mutable borrow occurs here
44 | foo.get().unwrap()
45 | }
| - immutable borrow ends here
error: aborting due to 5 previous errors
For more information about this error, try `rustc --explain E0502`.
error: Could not compile `playground`.
To learn more, run the command again with --verbose.
These are known limitations with the current borrowck algorithm. NLL fixes some of your cases but not the early return one - that requires Polonius as well; you can read more about that aspect in Baby Steps
Thank you for that (fast) answer. I was worried Rust was guarding me against some weird configuration I didn't think of.
So, while waiting for the Polonius borrow checker, do you think that writing return unsafe { &*(s_ref as *const _) } can be acceptable?
It should be sound, but whether it’s acceptable is in the eye of the beholder
Your example looks very similar in spirit to the motivation behind HashMap::entry(), so you can consider using a similar approach if none of the other safe alternatives work.
What if you set the lifetime of the returned string to be bound to the lifetime of the trait (instead of being the same)? Something like this? I do not know if this could be applied to your real-world situation...
To be honest, I initially tried using NLL, then I got the error about the lifetime of the returned str. I tried this solution without expecting it to work without NLL. But using the stable it compiles fine...
What I thought is that the compiler cannot return a reference to a lifetime that, since we put a new String inside Foo, must have a shorter lifetime than Foo. Otherwise you should not be able to replace it with a new one.
But this is just how I reasoned about the fact that it compiles without NLL and I did not expect it to work...
The issue that Polonius fixes is early returns extending the borrow out into the caller, thus preventing subsequent mutation in the function. So the issue is really that the borrow of foo is extended, and not related to the lifetime of the returned str.
So compiler can't really tell that we're "inserting" a String since these are just trait methods with a signature - it only goes by signatures and, similarly, cannot tell that we're replacing anything (in fact, I can't tell that either without seeing a specific impl ).
I think what will end up happening is you won't be able to implement this in the real case.
EDIT: Oh, I see @Yandros just replied above with essentially the same sentiment.
trait objects were here to alleviate the function signatures in the examples
I force myself to write self: &Self and self: &mut Self since sadly the &self and &mut self sugars are not consistent with rust reference pattern-matching, see https://github.com/rust-lang/rust/issues/52130
Ok, was just checking - some people new to Rust don't realize they're using trait objects (and all that entails) in cases like that.
Interesting perspective. FWIW, I never found the reference vs pattern usage of & to be confusing, but I can see how this is a YMMV scenario. I do think using the long form like you have is not canonical, and you'll likely not see it much (and conversely, someone looking at your code may find it unconventional). But ok, again, just wanted to make sure you were aware of the shorthand.
@vitalyd don't worry, my example code is poorly written (cough Foo bar baz cough) and your remarks are good (trait objects are an important subtlety indeed).
I raised the inconsistency issue when I realised that in a closure taking references (classic iteration situation) we write
|&x| { .. /* x is an owned value here, &x is the ref */ }
but when writing a method, I found myself wanting to write &self to feed a reference in the method body when the argument was &self.