Specialization question

I'm running into an issue where I have conflicting types, char and AsRef<str>:

trait Pushable<T> {
    fn push(&mut self, t: T) -> &mut Self;
}

impl<T> Pushable<T> for Vec<T> {
    fn push(&mut self, t: T) -> &mut Self {
        self.push(t);
        self
    }
}

impl<T: AsRef<str>> Pushable<T> for String {
    fn push(&mut self, t: T) -> &mut Self {
        self.push_str(t.as_ref());
        self
    }
}

impl Pushable<char> for String {
    fn push(&mut self, character: char) -> &mut Self {
        self.push(character);
        self
    }
}

fn push_and_return<T, U>(mut t: T, value: U) -> T where T: Pushable<U> {
    t.push(value);
    t
}

I understand that rustc is trying to future-proof my code here, but shouldn't I be able to opt-in to my own implementation?

If in the future, AsRef<str> is implemented for char, wouldn't the compiler still be able to overwrite the generic implementation just for char as I would assume that String::push(&mut self, ch: char) is faster than String::push_str(&mut self, string: &str) even when the &str has a length of 1?

I could just write a separate implementation for push_and_return just for char, but I'm doing this everywhere in a file and having one function really cleans things up for me. Any possible workarounds?

Update:
I found one workaround:

trait LocalAsRef<T: ?Sized>: AsRef<T> {}

Is there a better way to handle this? It would be nice if there were a procedural-macro where I could opt out of the compiler's implementation for a specific type like this:

#[except(char)]
impl<T: AsRef<str>> Pushable<T> for String { // ...

...but I'm no language designer, so I don't know what the implications would be.

What's happening here is that char doesn't implement AsRef<str>, so impl Pushable<char> for String doesn't specialize impl<T: AsRef<str>> Pushable<T> for String. You can't assume that char will never implement AsRef<str>, because they are both foreign types/traits, so these two impls conflict.

Thanks for the answer @RustyYato! I edited my original question for clarity and I also found a workaround, however, I'm not sure that it's the best workaround. Would you handle this with a local trait? Or is it more idiomatic to just write a separate function?

Negative reasoning + stability guarantees are hard, so negative reasoning is only permitted with local types/traits. The workaround you proposed will work, but it won't handle all types that implement AsRef<str>. Another way around this is to create a local type,

struct AsStr<T>(T);

impl<T: AsRef<str>> Pushable<AsStr<T>> for String {
    fn push(&mut self, AsStr(t): AsStr<T>) -> &mut Self {
        self.push_str(t.as_ref());
        self
    }
}

But this adds cognitive load to pushing, unfortunately these are the only two ways to get around this

1 Like

I'm going to opt for the trait and do this:

trait AsStr: AsRef<str> {}

impl AsStr for &String {}
impl AsStr for &str {}

I only need the two types so it's really not that bad.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.