Any `std` trait for taking a String or &str as a String?

Both String and &str meet ToString. So I can do fn f(a: impl ToString), but this allows any type other than String and &str too. Cow isn't a trait, so you'll have to type extra characters: Cow::from(str).

Is there anything like this in the std?

trait PrimitiveOrBoxedString {
    fn convert(&self) -> String;
}

impl PrimitiveOrBoxedString for &str {
    fn convert(&self) -> String {
        self.to_string()
    }
}

impl PrimitiveOrBoxedString for String {
    fn convert(&self) -> String {
        self.clone()
    }
}

Then you can do:

fn f(s: impl PrimitiveOrBoxedString) -> String {
    let s = s.convert();
    return s;
}

fn main() {
    println!("{}", f("Bonjour!"));
    println!("{}", f("Bonjour!".to_owned()));
}

Given your function receives an owned s: impl PrimitiveOrBoxedString, you probably want

trait PrimitiveOrBoxedString {
    fn convert(self) -> String;
}

impl PrimitiveOrBoxedString for &str {
    fn convert(self) -> String {
        self.to_string()
    }
}

impl PrimitiveOrBoxedString for String {
    fn convert(self) -> String {
        self
    }
}

instead.

This shortcoming was also present with a: impl ToString, but can be avoided using e.g. a: Into<String>. The latter of course still has a few more implementations than just for &str and String.

4 Likes

There's Into<String>, which still allows types other than &str and String but it's a bit more limited (for example it doesn't allow integers, which do however implement ToString)

3 Likes

You have trait bounds, but this way is not suggested. I prefer @steffahn 's.

fn f<T>(s: T) -> String 
where
    Cow<str>: From<T>
{
    Cow::from(s).into_owned()
}

fn main() {
    println!("{}", f("Bonjour!"));
    println!("{}", f("Bonjour!".to_owned()));
}

not tested. not convenient on my phone.

I'm not understanding -- how is allowing other types that implement ToString harmful in any way? Maybe you just want to know how to do this and that's Ok, but I'm curious about why.

1 Like

If you must do this, taking an impl Into<Cow<str>> can be interesting.

But personally I'd usually just say to take a String: the caller can put .to_owned() or .into() or .to_string() if needed.

(This is assuming you really do need a String. Obviously best of all is not needing ownership and just taking &str, but if you really need a String, just take a String. Don't take a &str and always copy it; that should be the caller's job since they might have a String already anyway.)

Generally, people trying to do this don't want random things like errors or integers passed in, as those are different-in-intent types.

8 Likes

This just looks a bit cryptic though.

In my case I'm thinking of node.set_meta_data("key", value) where key is not just &'static str because maybe meta-data may be dynamically injected.

One nice thing about types like Cow in Rust is that there's always the stdlib docs (available both online and installable through rustup) that neatly explain things. As for the impl Into<...> argument type, at this point that's as much an idiom in Rust for accepting a variety of argument types (to be treated as the same parameter type i.e. the target of the .into() call) as for i in mycollection { ... } is for iteration.

2 Likes

The hash table API in Rust looks similar and uses Borrow for functions that don't need to take ownership of a key. For the opposite direction you can use ToOwned.

Note that Borrow is a very strict trait, as it places requirements on Eq, Ord, and Hash implementations as well.

If you just want "something you can look at as a str", then you probably want AsRef<str> instead.

The generic version of accepting a Cow<str> would be either impl AsRef<str> + Into<String> or impl Into<Cow>. But many devs will suggest that you should just take Cow<str> directly instead; the caller can just write s.into() when providing an argument, and there's no need imposed to import Cow and write Cow::from.

It's very easy to be overeager to generalize function signatures for perceived caller ergonomics — I'm definitely not blameless here either — but adding generics also hurts clarity and ergonomics, especially since it weakens type inference and makes error messages on type mismatch more cryptic.

6 Likes

Won't Into<Cow> force the user to allocate compared to, say, impl AsRef<str>? Assuming they have a String or a &'static str

I feel like std lacks such a trait.

I have brought this up previously on IRLO and previously on URLO (edit: fixed link), with some frustrations caused and leading to some discoveries regarding AsRef and unfixable FIXMEs in core.

Note that AsRef isn't about owning vs borrowing but about type conversion (by reference). Also see the AsRef documentation, which states:

Used to do a cheap reference-to-reference conversion.

When it comes to borrowing vs owned values (instead of type conversion), the proper traits are in the std::borrow module: ToOwned and Borrow. But these traits lack a method like into_owned, which is only a method of the Cow type.

IMHO the correct (zero-cost) abstraction for taking a string as &str or String would be something like deref_owned::GenericCow, which provides an into_owned method as part of a trait (instead of type):

use deref_owned::GenericCow;
use std::borrow::Borrow;

fn generic_fn(arg: impl GenericCow<Borrowed = str>) {
    let reference: &str = &*arg; // or: arg.borrow()
    assert_eq!(reference, "Hello");
    let owned: String = arg.into_owned();
    assert_eq!(owned, "Hello".to_string());
}

(not available on Playground)

This isn't idiomatic, however, and the usual approach is to rather use std::borrow::Cow and accept the runtime overhead (with the gain of having less trouble with the syntax overhead of generics and only needing std types/traits).


Update:

Note that apart from being non-idiomatic, the above solution with GenericCow would require you to wrap a plain String on the caller side, as shown in this example. So it isn't optimal either. But the same holds when using Cow, where you also need to write Cow::Owned(owned_string) instead of just owned_string as an argument.

If you never need to unwrap the inner String (i.e. avoiding a clone in case of an owned String being passed and needed), you might simply use a Borrow<str> bound:

use std::borrow::Borrow;

fn foo<T: Borrow<str>>(arg: T) {
    println!("Got: {}", arg.borrow());
}

fn main() {
    foo("abc".to_string());
    foo("abc");
}

(Playground)

2 Likes

I guess another option would be to make your own trait and implement it for String and &str. But that's not a std trait then, and maybe also not very idiomatic.

Thanks for the suggestions... I'll end up not using this trait too or any solution above after all. I'll just use String for the meanwhile.

1 Like

If they have a String, they already allocated. If they have a &'static str, they won't have to allocate[1].


  1. variance will take care of any potential lifetime mismatch ↩ī¸Ž

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.