Borrow checker complaines only if lifetime is involved!?

I try to solve https://adventofcode.com/ using Rust to reactivate and improve my skills. Today I ran into an issue with the borrow checker which I did not understand. I spend hours trying to understand what's happening and the following code is the smallest minimal example I could come up with.

struct Map {}

#[derive(Clone, Copy)]
struct Pos<'a> {
    x: usize,
    y: usize,
    m: &'a Map
}

impl<'a> Pos<'a> {
    fn move_into(&self, _direction: &str) -> Option<Pos> {
        Some(
            Pos{
                x:self.x+1,
                y:self.y+1,
                m:self.m
            }
        )
    }
}

fn walk(start: &Pos) -> bool {
    let mut current = (*start).clone();

    for idx in 0..3 {
        current = match current.move_into("x") {
            Some(next) => {
                if next.x == 2 {
                    return true;
                }
                next
            },
            None => { return false; }
        };
    }

    false
}

fn main() {
    let m = Map{};
    let pos = Pos{x:0, y:0, m: &m};
    walk(&pos);
}

The borrow checker complains about the current = match current.move_into ... in the walk function and tells me, that I cannot do that because current is borrowed. The following code has Map and related lifetimes removed. Otherwise it is the same.

#[derive(Clone, Copy)]
struct Pos {
    x: usize,
    y: usize,
}

impl Pos {
    fn move_into(&self, _direction: &str) -> Option<Pos> {
        Some(
            Pos{
                x:self.x+1,
                y:self.y+1,
            }
        )
    }
}

fn walk(start: &Pos) -> bool {
    let mut current = (*start).clone();

    for idx in 0..3 {
        current = match current.move_into("x") {
            Some(next) => {
                if next.x == 2 {
                    return true;
                }
                next
            },
            None => { return false; }
        };
    }

    false
}

fn main() {
    let pos = Pos{x:0, y:0};
    walk(&pos);
}

This code is ok and works. I don't have the slightest idea how the lifetime / reference in the struct affects the borrowing in my function. Could somebody please explain to me what's going on here?

Right here, you've returned a different Pos type than you started with, because you elided its lifetime. The lifetime elision rules produce the following full set of lifetimes for your function signature (because elided lifetimes in the return type always borrow from self if there is a self to borrow from):

impl<'a> Pos<'a> {
    fn move_into<'x, 'y>(&'x self, _direction: &'y str) -> Option<Pos<'x>> {

Therefore your returned Pos has a shorter lifetime than the original — it borrows from the input Pos — and can't be assigned in place of it. To get the result you want, you can write the lifetime explicitly...

impl<'a> Pos<'a> {
    fn move_into(&self, _direction: &str) -> Option<Pos<'a>> {

or do it the way I prefer, with Self which always means the type named in the impl:

impl<'a> Pos<'a> {
    fn move_into(&self, _direction: &str) -> Option<Self> {
3 Likes

Well, it's not surprising that removing a borrowing field changes the borrow checker analysis. I'll just concentrate on the erroring version.

The first thing I do when someone asks about a borrow checker error is to add this to their code:

#![deny(elided_lifetimes_in_paths)]

Doing this for your code highlights two positions where the presence of a lifetime is completely elided:

impl<'a> Pos<'a> {
    fn move_into(&self, _direction: &str) -> Option<Pos> {
    //                                              ^^^
fn walk(start: &Pos) -> bool {
//              ^^^

This is what they are short for:

impl<'a> Pos<'a> {
    fn move_into(&self, _direction: &str) -> Option<Pos<'_>> {
    //                                              ^^^^^^^
fn walk(start: &Pos<'_>) -> bool {
//              ^^^^^^^

The second case, in walk, means that the reference and the Pos<'_> have separate lifetimes.[1] That's what you want, so you can just go with Pos<'_>.

The first case, in move_into, is subject to method signature lifetime elision. It's short for this:

impl<'a> Pos<'a> {
    fn move_into<'borrow>(&'borrow self, _direction: &str) -> Option<Pos<'borrow>> {

That means that the *self which is Pos<'a> will remained borrowed for as long as the the returned Pos<'borrow> is in use. That's the source of the error about overwriting current.

So what you want is for the 'borrow to expire immediately after the call, and to return another Pos<'a>. Thus this fixes the error:

impl<'a> Pos<'a> {
    fn move_into(&self, _direction: &str) -> Option<Pos<'a>> {

Or alternatively,

impl<'a> Pos<'a> {
    fn move_into(&self, _direction: &str) -> Option<Self> {

When you get a borrow checker error, I recommend using #![deny(elided_lifetimes_in_paths)] and thinking through how you want the borrows to work at every place that errors.

(When learning Rust, if you can not put borrows into your structs, even better. Learners tend to overuse references.)


  1. fn walk<'r, 'pos>(start: &'r Pos<'pos>) -> bool ↩ī¸Ž

2 Likes

@quinedot and @kpreid Thanks for your kind explanations! It took me a while to understand why it results that error message, but I think I got it now. And learned quite something. Your help is highly appreciated! :slight_smile: