Search code examples
referencerustlifetimeborrow-checkerborrowing

How to use `AsRef` arguments?


I am having a hard time trying to get AsRef to work in a clean way.

const DEFAULT: &str = "lib";

use std::path::{Path, PathBuf};

fn extend(p: &Path, q: Option<&Path>) -> PathBuf {
    let q: &Path = q.unwrap_or(DEFAULT.as_ref());
    p.join(q)
}

// DOES NOT COMPILE
fn extend1<P: AsRef<Path>, Q: AsRef<Path>>(p: P, q: Option<Q>) -> PathBuf {
    let q: &Path = q.map(|x| x.as_ref()).unwrap_or(DEFAULT.as_ref());
    p.as_ref().join(q)
}

// DOES NOT COMPILE
fn extend2<P: AsRef<Path>, Q: AsRef<Path>>(p: P, q: Option<Q>) -> PathBuf {
    let q: &Path = match q {
        Some(x) => x.as_ref(),
        None => DEFAULT.as_ref(),
    };
    p.as_ref().join(q)
}

fn extend3<P: AsRef<Path>, Q: AsRef<Path>>(p: P, q: Option<Q>) -> PathBuf {
    match q {
        Some(x) => p.as_ref().join(x.as_ref()),
        None => p.as_ref().join(AsRef::<Path>::as_ref(DEFAULT)),
    }
}

The function extend works with Path references, but I want to generalize it to accept arguments of type AsRef<Path> in order to allow also string, for instance.

My first attempts, extend1 and extend2, do not pass the borrow checker, which complains about the lifetime of x in both cases.

My third attempt, extend3, works, but has the obvious drawback of code duplication, which gets more severe as the function body grows.

What is the best solution in this circumstance?


Solution

  • Option::map consumes the inner value (if there is one). So in the code q.map(|x| x.as_ref()), the closure takes x by value. You can check this by noting that q.map(|x: &Q| x.as_ref()) gives a type error, while q.map(|x: Q| x.as_ref()) does not (it still gives the lifetime error). That means that when you call x.as_ref(), a new reference to x is created, not related to any outside reference. This means that the reference is only valid inside the closure, but you want to use it in the rest of extend1.

    What you want to do instead is have a borrow of q that's valid until the end of extend1. This borrow of q can be turned into a borrow of its contents (if any) using Option::as_ref() to convert &Option<Q> into Option<&Q> (simply using q.as_ref() will create the borrow of q you need). Then when you use map, the closure will take &Q, rather than Q. The lifetime of the reference will be the same as the external borrow of q, so it will last until the end of extend1 (if needed).

    const DEFAULT: &str = "lib";
    
    use std::path::{Path, PathBuf};
    
    fn extend1<P: AsRef<Path>, Q: AsRef<Path>>(p: P, q: Option<Q>) -> PathBuf {
        let q: &Path = q.as_ref().map(|x| x.as_ref()).unwrap_or(DEFAULT.as_ref());
        p.as_ref().join(q)
    }
    

    (playground)

    Since the arguments to your function are only ever used by reference, you may wish to only take them by reference. This is as easy as adding an ampersand to each argument.

    const DEFAULT: &str = "lib";
    
    use std::path::{Path, PathBuf};
    
    fn extend1<P: AsRef<Path>, Q: AsRef<Path>>(p: &P, q: &Option<Q>) -> PathBuf {
        let q: &Path = q.as_ref().map(|x| x.as_ref()).unwrap_or(DEFAULT.as_ref());
        p.as_ref().join(q)
    }
    

    (playground)

    or

    const DEFAULT: &str = "lib";
    
    use std::path::{Path, PathBuf};
    
    fn extend1<P: AsRef<Path>, Q: AsRef<Path>>(p: &P, q: Option<&Q>) -> PathBuf {
        let q: &Path = q.map(|x| x.as_ref()).unwrap_or(DEFAULT.as_ref());
        p.as_ref().join(q)
    }
    

    (playground)

    Note that this last version doesn't need to call q.as_ref(), since q already has type Option<&Q>. This version most closely follows your original extend function.