Functions in std can take and return ref but my function cannot

Hi everybody,

I noticed that functins in std::string can be easily chained like this:

"abc".strip_prefix("a").unwrap().strip_suffix("c").unwrap();

But for some reason, my function remove_edges() cannot be chained:

pub trait StringUtils {
    fn remove_edges(&self, delimiter: &str) -> &str;
    fn split_in_middle(&self, separator: &str) -> Option<(&str, &str)>;
}
impl<T> StringUtils for T
where
    T: AsRef<str>,
{
    fn remove_edges(&self, separator: &str) -> &str {
        let s = self.as_ref();
        s.strip_prefix(separator).unwrap_or(s).strip_suffix(separator).unwrap_or(s)
    }
    fn split_in_middle(&self, separator: &str) -> Option<(&str, &str)> {
        let s = self.as_ref();

        // this works:
        // s.strip_prefix(separator)
        //     .unwrap_or(s)
        //     .strip_suffix(separator)
        //     .unwrap_or(s)
        //     .split_once(separator)

        // this does not work:
        s.remove_edges(separator).split_once(separator)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_split_in_middle() {
        let out = "|abc|def|".split_in_middle("|");
        assert_eq!(out, Some(("abc", "def")));
    }
}

You don't call <T as StringUtils>::remove_edges but <&str as StringUtils>::remove_edges in your split_in_middle method when you use self.as_ref() instead of self itself. AsRef::as_ref doesn't return a reference that lives as long as the implementing reference type, but instead creates a new temporary reference that lives only till the end of split_in_middle. Your test works fine when I replace the temporary &str reference as the receiver of remove_edges with self.

Edit: I think my reasoning as to what causes the compiler error was wrong. It is not the lifetime of the &str reference returned from calling self.as_ref(), but I think the problem is a double borrow &'temporary &'a str. The outer &'temporary reference is created during method call resolution of remove_edges. The outer temporary reference is needed because only &'a str implements remove_edges, not str itself. If we add an implementation of StringUtils for str, we can pass the &'a str instance directly to remove_edges, without having to create a temporary reference. Here an example of what I mean, I'm quite unsatisfied with my explanation, I hope the code makes more sense.

1 Like

Great explanation @jofas . Thank you!

I wonder, is it somehow possible to chain it like this:

pub trait StringUtils {
    fn remove_left(&self, delimiter: &str) -> &str;
    fn remove_right(&self, delimiter: &str) -> &str;
    fn remove_left_and_right(&self, delimiter: &str) -> &str;
    fn split_in_middle(&self, separator: &str) -> Option<(&str, &str)>;
}
impl<T> StringUtils for T
where
    T: AsRef<str>,
{
    fn remove_left(&self, separator: &str) -> &str {
        let s = self.as_ref();
        s.strip_prefix(separator).unwrap_or(s)
    }
    fn remove_right(&self, separator: &str) -> &str {
        let s = self.as_ref();
        s.strip_suffix(separator).unwrap_or(s)
    }
    fn remove_left_and_right(&self, separator: &str) -> &str {
        // error:
        self.remove_left(separator).remove_right(separator)
    }
    fn split_in_middle(&self, separator: &str) -> Option<(&str, &str)> {
        self.remove_left_and_right(separator).split_once(separator)
    }
}

Just change it to impl<T: ?Sized> and everything works.

2 Likes

Cool! Thank you @steffahn !

Did ?Sized somehow fixed the double borrow issue?

pub trait StringUtils {
    fn remove_left(&self, delimiter: &str) -> &str;
    fn remove_right(&self, delimiter: &str) -> &str;
    fn remove_left_and_right(&self, delimiter: &str) -> &str;
    fn split_in_middle(&self, separator: &str) -> Option<(&str, &str)>;
}
impl<T> StringUtils for T
where
    //////////////// FIX //////////////////
    T: AsRef<str> + ?Sized, // fixes "returns a value referencing data owned by the current function"
    //////////////// FIX //////////////////
{
    fn remove_left(&self, separator: &str) -> &str {
        let s = self.as_ref();
        s.strip_prefix(separator).unwrap_or(s)
    }
    fn remove_right(&self, separator: &str) -> &str {
        let s = self.as_ref();
        s.strip_suffix(separator).unwrap_or(s)
    }
    fn remove_left_and_right(&self, separator: &str) -> &str {
        self.remove_left(separator).remove_right(separator)
    }
    fn split_in_middle(&self, separator: &str) -> Option<(&str, &str)> {
        self.remove_left_and_right(separator).split_once(separator)
    }
}

When str implements the trait, you can call the methods in &self == &str. Without it, it's still implemented for &str, so it automatically adds a reference indirection and you'll call on &self == &&str.

3 Likes

I very much appreciate the explanation but I don't understand that. Is this discussed somewhere the Rust book so I can educate myself?

To understand why it calls the method on &str vs &&str, you'll need to know about traits in general, look at the existing implementations of AsRef (in particular also this one) and understand the basics of dynamically sized types and of method resolution. To understand why the double indirection breaks the lifetimes, understanding lifetime elision rules might help. As you see by these links, for a full picture of advanced features, besides the book, useful resources can be the standard library docs, the nomicon, and the reference.

1 Like