Curious scope rules when using `if let`


#1

Hey there! Apologies if this has already been covered – I am extremely new to rust, so this is probably a pretty elementary question. I have a simplified example of an “upsert” function which runs exactly as expected.

use std::collections::BTreeMap;

#[derive(Debug)]
struct Thing {
    name: String,
    version: u8
}

fn upsert(map: &mut BTreeMap<char, Thing>, id: char, name: String) {

    // perform an update
    if !match map.get_mut(&id) {
        Some(thing) => {
            thing.name = name.clone();
            thing.version = thing.version + 1;
            true
        },
        None => false
    }

    // perform an insert
    {
        map.insert(id, Thing{ name: name.clone(), version: 1 });
    }
}

fn main(){


    let mut map: BTreeMap<char, Thing> = BTreeMap::new();
    map.insert('a', Thing{ name: "Alpha".to_string(), version: 1});
    map.insert('b', Thing{ name: "Beta".to_string(),  version: 1});
    map.insert('d', Thing{ name: "Delta".to_string(), version: 1});




    // performs an update
    upsert(&mut map, 'b', "Bravo".to_string());

    // performs an insert
    upsert(&mut map, 'c', "Charlie".to_string());


    println!("{:?}", map);

}

however, I initially wanted (and would still prefer) to write the upsert function without those booleans, which feel really clunky when the following should (based on my understanding) be safe:

fn upsert(map: &mut BTreeMap<char, Thing>, id: char, name: String) {

    // perform an update
    if let Some(thing) = map.get_mut(&id) {
        thing.name = name.clone();
        thing.version = thing.version + 1;
    }

    // perform an insert
    else {
        map.insert(id, Thing{ name: name.clone(), version: 1 });
    }
}

However, this throws an Error: cannot borrow `*map` as mutable more than once at a time... at the map.insert. It appears that the lifetime of thing ends when upsert ends; but shouldn’t it end when the conditional expression ends? The variable isn’t available outside that expression:

fn upsert(map: &mut BTreeMap<char, Thing>, id: char, name: String) {

    // perform an update
    if let Some(thing) = map.get_mut(&id) {
        thing.name = name.clone();
        thing.version = thing.version + 1;
    }

    // Error: unresolved name `thing`...
    thing;
}

Am I totally missing something here? Thanks in advance!


#2

Borrows made in the expression of an if let persist until the end of the entire construct, including the else clause. I don’t think anyone is particularly enthusiastic about this, but since you can work around it, I don’t think it’s a priority issue.

Either way, you probably want the BTreeMap::entry method for things like this.


#3

Oh wow, thanks for that link – that’s exactly what I need here. I had a sense that while the borrow checker should ideally support this syntax, the fact that it didn’t meant there was probably a more idiomatic way to accomplish such things. I am beginning to see that a semi-functional style might be a bit of a preference in Rust. Is that a fair statement?


#4

Basicly, yes. Though pure functional approach is often much less performant: Rust is not fully functional language after all.

As for initial issue, I believe it’s a known one. I couldn’t find an issue in github, but I think there should be one. I hope the issue will be resolved some time soon.


#5

Great to know. For anyone else who stumbles across this thread later on, I think this might be addressed by https://github.com/rust-lang/rfcs/issues/811.