Contrapositive of `AsRef<str>` for convently taking owned `String`?

We use AsRef<str> all the time for convenience. It reduces the syntax required on the caller side, when the function only cares about being able to read the value as a string.

This is how I think about the following contracts:

  • &str: Accept only a borrowed string, either from owned (&String) or static (&'static str).
  • AsRef<str>: Accept any value that can behave like a borrowed string (&str).
  • String: Accept only an owned String from runtime.
  • Cow<str>: Accept either a reference to a string that may be cloned, or an owned string. If it is a reference, it may be cloned deterministically, if it is owned, it will be mutated and moved somewhere else.

Please correct me, I'm sure my understanding is rather flawed.

But what I'm here to discuss is the contrapositive of AsRef<str>. If it means "take any type that can act as a reference to a string value", what is the trait bound for "take any type that can be coerced into an owned String, cloning if necessary, moving if not"?

This sounds like I'm asking for Cow<str>, but I don't believe this is the case. Cow is harder to use. The caller must wrap their value in this type, and pass that to a function. What if a function wants to operate on a type similar to Cow, but it requires ownership in all circumstances? The caller can choose to provide owned data, or it can be a reference to something that can be cloned into a String.

Here is the behavior that I have in mind for several potential types of input:

  • &str: Clone the value behind the reference into String.
  • AsRef<str>: Ditto.
  • String: Take ownership of the value.
  • Cow<str>: If it is borrowed, clone the value. If it is owned, make the cow poop it out.

There are several traits that fit the desired contracts:

  • Into<String>: This may be too lose, but generally as I understand this trait implies a 1:1 map between the original from type and the String, meaning that no information is lost, it is a different representation. This does seem like the best option to me.
  • ToString: This probably isn't what we want. It could be, but only if T: ToString + !Display, because we probably don't want to be mutating the (lossy) representation that is usually returned from the blanket impl that comes with Display. Display is intended for just that: display to a human. It is not directly a conversion, it is a summary.
  • ToOwned<String>: I don't know what to think about this one. The name of the trait seems to be an exact description of how I would like the generic to behave. If the value is already owned, using .to_owned() does nothing and there is no wasted work. If the value is a reference, ToOwned implementation handles the behavior, which is usually to delegate to Clone.

What do you all think? How can I make passing an owned String, with as loose a contract as possible, convenient to do?

It might be GenericCow<str> (edit: see example), but …

Edit: Also GenericCow<str> implies Borrow<str>, which is stricter than AsRef<str>. Thus GenericCow is the corresponding match for Borrow instead of AsRef.

Edit #2: On the other hand, Cow<'_, str> and GenericCow<str> are more generic as they support borrowing while you only wanted to require ownership in all cases. Thus Cow/GenericCow are different concepts: 1. borrowing (where Eq, Ord, and Hash behave consistent) instead of cheap conversion (where Eq, Ord, and Hash do not have to behave consistent), and 2. supporting both directions (either to a reference or to an owned value, while you only want an owned value).

I would say then the equivalent to AsRef<str> is Into<String>, like you said. Note: Both are in std::convert.

I think it's this.

This is sometimes more lax, sometimes more strict (I think). More lax because It will accept everything with a string representation due to impl<T: ?Sized> ToString for T where T: Display + ?Sized, see implementators of ToString. More strict because it will not accept anything everything that is Into<String>, right?

This clones unnecessarily. See also this long thread.

Usually, people just use Into<String> for this purpose.

3 Likes
  • Into<<T as ToOwned>::Owned> relates to AsRef<T>

just like

  • GenericCow<T> relates to Borrow<T>

Update: I'm no longer sure if Into is really the counterpositive of AsRef in the general case. That's because every T implements Into<T> but not every T implements AsRef<T>.

fn foo(arg: impl AsRef<str>) {
    let _: &str = arg.as_ref();
}

fn bar(arg: impl AsRef<i32>) {
    let _: &i32 = arg.as_ref();
}

fn main() {
    foo("Hello");
    // Does not work:
    // bar(&5);
}

(Playground)

In the concrete case of String and str, however, I think it is.

See this playground example:

struct Foo;

impl From<Foo> for String {
    fn from(_: Foo) -> Self {
        "Foo".to_string()
    }
}

fn bar(arg: impl Into<String>) {
    let owned_string: String = arg.into();
    println!("{owned_string}");
}

#[allow(dead_code)]
fn baz(arg: impl ToString) {
    let owned_string: String = arg.to_string();
    println!("{owned_string}");
}

fn main() {
    bar(Foo);
    // This doesn't work:
    // baz(Foo);
}

(Playground)

Output:

Foo


AsRef and Into are in std::convert.

ToString, however, is in std::fmt.

If you ask for the contrapositive of AsRef<str>, I would definitely go for Into<String>.