Does not Rust compiler evaluate arguments strictly?


#1

Hi.

When I implement accessor methods, I always wonder why the function “fail” below cause an error even if the function “success” below is ok.
Does Rust compiler do kind of lazy evaluation?

struct Example {
    f: i32,
}

impl Example {
    fn get(&self) -> i32 { self.f }
    fn set(&mut self, value: i32) { self.f = value; }
}

fn fail() {
    let mut x = Example { f: 0 };
    x.set(x.get() + 1); // ERROR: cannot borrow 'x' as immutable because it is also borrowed as mutable
}

fn success() {
    let mut x = Example { f: 0 };
    let f = x.get();
    x.set(f + 1);
}

#2

I guess

x.set(x.get() + 1);

like this:

set(&mut self, x.get());

The self and x in the method parameters are all mutable in a same scope. So this break the principles about mutable reference.

First, any borrow must last for a scope no greater than that of the owner.
Second, you may have one or the other of these two kinds of borrows, but not
both at the same time:
one or more references (&T) to a resource,
exactly one mutable reference (&mut T).
https://doc.rust-lang.org/stable/book/references-and-borrowing.html

However, I am not sure about that:grin:
Hope someone can help:relaxed:


#3

This is a longstanding weakness in the compiler. I can’t seem to find the relevant issue though.


#4

wxwhaha-san, sfackler-san, thank you!

Now I understand that the compiler can not determine the order of evaluation of arguments including self properly yet.

Hope future improvement.


#5

Data structures in Rust have an entry API for this very reason – you’d write c.entry(x) += 1 instead of c.set(x, c.get(x) + 1). Look at std::collections::HashMap to see how it works.


#6

Isn’t it this one: https://github.com/rust-lang/rfcs/issues/811 ? (Non-lexical borrow scopes and better treatment of nested method calls)


#7

Thank you!
I have never known entry.
It seems nice in the view point of coding style.
Since main interest is performance in my current case, HashMap instead of struct is not acceptable.

But I am glad to know it!


#8

You don’t need to use a HashMap. I meant you should implement the entry API (or something similar) for your struct. I noted the HashMap implementation as example to guide your own implementation.


#9

Sorry for my misunderstanding.

Though still I don’t study entry API, it is possible to use for different fields? Such as

struct Example {
    f1: i32,
    f2: i32,
}

...
    self.set_f1(self.get_f2() + 1);

#10

That would be self.f1() += 1.


#11

I guess

x.set(x.get() + 1);

like this:

set(&mut self, x.get());

More specifically, it’s this

Example::set(&mut x, Example::get(&x));

This, of course, involves two borrows.


#12

And for the more generic case where one is not just incrementing, separate the borrow lifetimes more explicitly for the compiler by writing it as:

let val = self.get_f2() + 1;
self.set_f1(val);

edit: Uh…which I just noticed was already figured out in the original question.


#13

Yet another alternative would be to alter the value inside a closure: (playground link)

struct Example {
    f: i32,
}

impl Example {
    fn mapf<F>(&mut self, func: F) where F : FnOnce(i32) -> i32 { self.f = func(self.f); }
    fn get(&self) -> i32 { self.f }
}

fn main() {
    let mut x = Example { f: 0 };
    println!("{}", x.get());
    
    x.mapf(|f| f+1);
    
    println!("{}", x.get());
}

Which happens to be shorter than the x.set(x.get()... style, if that’s what you’re looking for. Though I’m not so sure about the name, mapf(), it takes a closure just like mapping a collection, but it only applies it to one item.