What is the purpose of AsRef?

I was checking the phone number crate and I came across with the function signature

pub fn is_viable<S: AsRef<str>>(string: S) -> bool

Here what is the point of AsRef how is it different from using &str ?

1 Like

It’s a slight convenience for the caller of the function. You can pass either a normal &str, or anything else that meets the AsRef<str> trait bound. Most notably, String meets that bound, so this allows you to call the same function on either a &str or a String.

(If the signature was just pub fun is_viable(string: &str) -> bool, and all you had to call that function on was a String, you would need to call .as_ref() on your string first in order to make it a &str you could pass to is_viable. So it’s nicer if is_viable does that for you.)

2 Likes

that sounds cool. and as a writer how do you handle the param string of type S? can I use it as a drop in replacement for the place I use &str?

The only thing you know about S here is that it is a type that implements AsRef<str>. (Because of the S: AsRef<str> in the type signature.)

So the one and only thing you’re allowed to do with a parameter of type S in this case it is to use methods from the AsRef trait. AsRef has exactly one method, .as_ref(). So the only thing the implementer of is_viable can do is use .as_ref() on that parameter, getting (in this case) a value of type &str.

So if you’re implementing a function, and you intend for your function to take a &str argument, you might instead have it take a generic argument S: AsRef<str>, and then call .as_ref() on that argument to get the &str you needed. As the implementer of the function, either way you end up with a supplied &str, but the generic version makes it slightly more ergonomic for callers who have not a &str but, say, a String. (Because you’re the one converting it for them, and they don’t have to think about the conversion at all.)

5 Likes

I see thank you :pray: :slight_smile:

Using AsRef<str> most often than not will just allow the caller to avoid having to type a leading & in front of their expression, since most stuff that is AsRef<str> is also Deref<Target = str> (e.g., coercing a &String to a &str).

This, then has a few minor drawbacks to be aware of:

  • The function can take ownership of an input String when what it really needed was just a borrow; this may lead a beginner into .clone-ing needlessly.

    For instance,

    fn print_str<S> (s: S)
    where
        S : AsRef<str>,
    {
        let s: &str = s.as_ref();
        println!("{}", s);
    }
    
    fn main ()
    {
        let s = String::from("Hello, World!");
        print_str(s);
        print_str(s);
    }
    

    errors with

    error[E0382]: use of moved value: `s`
      --> src/main.rs:11:15
       |
    9  |     let s = String::from("Hello, World!");
       |         - move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait
    10 |     print_str(s);
       |               - value moved here
    11 |     print_str(s);
       |               ^ value used here after move
    

    and a beginner may be tempted to fix this with:

    fn print_str<S> (s: S)
    where
        S : AsRef<str>,
    {
        let s: &str = s.as_ref();
        println!("{}", s);
    }
    
    fn main ()
    {
        let s = String::from("Hello, World!");
        print_str(s.clone());
        print_str(s);
    }
    
  • Given how a generic function works (a generic function is actually a function factory; for every different type <S> the generic is called with, Rust will monomorphise the generic into a new concrete function (print_str::<String>, print_str::<&str>, print_str::<Rc<str>>), i.e., compile a new version of the function), this leads to a little bit longer compile times and potentially bigger binaries. This can be fixed with the following pattern, (done, for instance, by ::momo's #[momo] attribute macro):

    fn foo<S> (s: S)
    where
        S : AsRef<str>,
    {
        let s: &str = s.as_ref();
        /*
         *  VERY
         *  LONG
         *  FUNCTION
         *  '
         *  S
         *  BODY
         */
    }
    

    becomes

    #[inline]
    fn foo<S> (s: S) // same public prototype
    where
        S : AsRef<str>,
    {
        fn foo_common (s: &str)
        {    
            /*
             *  VERY
             *  LONG
             *  FUNCTION
             *  '
             *  S
             *  BODY
             */
        }
    
        foo_common(s.as_ref())
    }
    

This being said, the trait is indeed useful: imagine that you want to take a sequence of strings (to, let’s say, print them all):

  • How do you express the “sequence-ness”?
    Vec<..> is too restrictive (in the same way as taking a String instead of a &str is), so you can take a impl AsRef<[..]>, in the same vein as you were taking a impl AsRef<str>. As I said, there is little benefit in this case, where taking a &[..] is just as fine.

  • How do you express the “string-ness”?
    String is too restrictive (e.g., it wouldn’t accept &["Hello", "World!"]), but it turns out that so is &str! Indeed, taking a &[&str] does not accept a
    &[String::from("Hello"), String::from("World!")]
    .

    This is where things like AsRef really do shine: by taking a &[impl AsRef<str>], we manage to have a function accepting both cases!

    fn print_strs (strs: &[impl AsRef<str>])
    {
        println!("{}", ::itertools::Itertools::format(
            strs.iter().map(AsRef::as_ref),
            ", ",
        ));
    }
    
    fn main ()
    {
        print_strs(&[
            "Hello",
            "World!",
        ]);
        print_strs(&[
            "Hello".to_string(),
            "World!".to_string(),
        ]);
    }
    
(and for the record, here is the most general input this kind of function can take)
8 Likes