Is it possible to implement trait method with where clause on a concrete type?

Hello everyone!

Suppose I have a trait that looks like this:

trait Wrapper {
    type Inner;
    fn set(&mut self, v: Self::Inner);
    fn mutate(&mut self, f: impl FnOnce(&mut Self::Inner))
    where
        Self::Inner: Clone;
}

I could implement it using generic struct like this:

struct Wrapped<T>(T);
impl<T> Wrapper for Wrapped<T> {
    type Inner = T;
    fn set(&mut self, v: T) {
        self.0 = v;
    }
    fn mutate(&mut self, f: impl FnOnce(&mut T))
    where
        T: Clone,
    {
        let mut clone = self.0.clone();
        f(&mut clone);
        self.0 = clone;
    }
}

#[derive(Clone)]
struct Clonable;
// <ContainsClonable as Wrapper>::mutate exists
type ContainsClonable = Wrapped<Clonable>;

struct NonClonable;
// <ContainsNonClonable as Wrapper>::mutate doesn't exist
type ContainsNonClonable = Wrapped<NonClonable>;

But I can't use that approach in my situation. Instead, I need to do something like this:

struct ContainsClonable(Clonable);
impl Wrapper for ContainsClonable {
    type Inner = Clonable;
    fn set(&mut self, v: Self::Inner) {
        self.0 = v;
    }
    fn mutate(&mut self, f: impl FnOnce(&mut Self::Inner)) {
        // this is a simplification, of course
        let mut clone = self.0.clone();
        f(&mut clone);
        self.0 = clone;
    }
}

struct ContainsNonClonable(NonClonable);
impl Wrapper for ContainsNonClonable {
    type Inner = NonClonable;
    fn set(&mut self, v: Self::Inner) {
        self.0 = v;
    }
    fn mutate(&mut self, f: impl FnOnce(&mut Self::Inner))
    where
        Self::Inner: Clone
    {
        let mut clone = self.0.clone();
        f(&mut clone);
        self.0 = clone;
    }   
}

But this code doesn't compile for the ContainsNonClonable, because where clause in it's impl Wrapper block doesn't make the compiler ignore this method like it does in the impl Wrapper block of the generic Wrapped<T>.

I wonder if it's possible to somehow make it work? I'm aware that I can do something like this:

trait Wrapper {
    type Inner;
    fn set(&mut self, v: Self::Inner);
    fn mutate(&mut self, f: impl FnOnce(&mut Self::Inner))
    where
        Self::Inner: Clone
    {
        unimplemented!()         
    }
}

struct ContainsNonClonable(NonClonable);
impl Wrapper for ContainsNonClonable {
    type Inner = NonClonable;
    fn set(&mut self, v: Self::Inner) {
        self.0 = v;
    }
}

But
a) It requires explicit remove of mutate method implementation (which I can't do since both ContainsClonable and ContainsNonClonable are generated with one macro);
b) It makes ContainsNonClonable::mutate available thing that just panics, which is less than ideal.

Any ideas? I hope the solution doesn't require specialization...

The actual requirement is lost amongst all the code you posted. Please formulate your high-level requirement concisely.

My high-level question is in the title of the post: I want to know if it's possible to ignore a method implementation based on the where clause on a concrete type. I acknowledge the fact that this question may be not clear, therefore I created little code examples that describe my problem and my thought process. I'm not sure how I can do this better.

I think the where clause does exactly that: if it's on the method, then the implementation will not be available.

I'm curious why

I can't use that approach in my situation

because in the playground it seems to work as advertised.

In the current version of my crate it works like in the playground you linked. This, however, has several disadvantages, therefore I decided to move to the new API. Basically it exports two items: a Wrapper trait and a declarative macro that implements the trait for your type. Basically, I need this to work:

But it doesn't. Therefore I wonder if it's just my lack of knowledge and there's a workaround or I just want something impossible (which maybe has an RFC, that would be interesting).

I think, I just can't understand why this works:

struct Wrapped<T>(T);
impl<T> Wrapper for Wrapped<T> {
    type Inner = T;
    // ...
    fn mutate(&mut self, f: impl FnOnce(&mut T))
    where
        T: Clone // this won't be the case and the whole method will
                 // be ignored
    {
        // ...
    }
}

And this not:

struct NonClonable;
struct Wrapped(NonClonable);
impl Wrapper for Wrapped {
    type Inner = NonClonable;
    // ...
    fn mutate(&mut self, f: impl FnOnce(&mut NonClonable))
    where
        NonClonable: Clone // this will throw an error
    {
        // ...
    }
}

That doesn't work because NonClonable: !Clone, so it's a compile-time contradiction. It's basically almost the same reason why let x: i32 = "string"; doesn't work, although not quite. I guess this particular issue is more of a design decision, as opposed to something fundamental – it could technically work (in which case the method just wouldn't ever be usable), but it's probably forbidden deliberately because it's surprising and/or bad style.

Thanks, I will have a look at that example.

I’d call this a limitation of what the rust compiler is currently capable of. I hope we’ll eventually get improvements here, but those things can take a while. [1] Be glad that an approach of using something like

impl Wrapper for ContainsNonClonable {
    type Inner = NonClonable;
    fn set(&mut self, v: Self::Inner) {
        self.0 = v;
    }
    fn mutate(&mut self, f: impl FnOnce(&mut Self::Inner)) {
        unreachable!();
    }   
}

at least still works at all. Other cases are not as straightforward, e.g. a trait

trait Foo {
    type FooTy;
    fn method() -> <Self::FooTy as Tr>::TrTy
    where
        Self::FooTy: Tr;
}

with

trait Tr {
    type TrTy;
    fn make() -> Self::TrTy;
}

can be implemented generically, e.g.

impl<T> Foo for T {
    type FooTy = T;
    fn method() -> <Self::FooTy as Tr>::TrTy
    where
        Self::FooTy: Tr,
    {
        T::make()
    }
}

but for a concrete type, the same implementation fails

struct T;
impl Foo for T {
    type FooTy = T;
    fn method() -> <Self::FooTy as Tr>::TrTy
    where
        Self::FooTy: Tr,
    {
        T::make()
    }
}

but this case there’s even no way to remove the Self::FooTy: Tr bound and use unreachable!() either, because it’s necessary for the return type to make sense.


By the way, there one details to note: if your NonClonable type is not local to the crate (but the trait is), then you couldn’t rely on the fact that NonClonable doesn’t implement Clone (because adding such an impl is a non-breaking change). So instead of the compiler recognizing that “the method can be ignored”, it might be better to give it the ability to just “assume NonClonable: Clone” locally within the method in question. It shouldn’t matter if such an assumption is contradictory as long as the function is not even callable. Maybe both make sense though; I would definitly like to be able to just not implement the method at all in cases such as your example where the trait bounds it has are known to be impossible to satisfy, based on information local to the crate only.


  1. as far as I know, the trait system is a complex beast, and there’s already ongoing efforts to re-write it anyways, which might make changes like this easier, too, I guess? (but that’s just a guess...) ↩︎

Maybe I'm wrong, but I have an impression that Wrapper<NonClonable> boils down to the same thing, so the where T: Clone turns into where NonClonable: Clone, which is obviously not the case, but instead of throwing a compile error it just ignores the method: "oh, NonClonable is not Clone, so I dont need to implement this method"

Thanks for this info. I also hope that chalk will improve trait situation (maybe specialization will come, huh?). I didn't get, however, this part:

Maybe you can give a code example?

As a solution for my particular problem (a crate which provides macro that implements the crate's trait for a local type), I think I'm left with only one option: introduce additional trait, something like WrapperClonable which will have only the mutate method. Then, when I use the macro, I should explicitly indicate that I want to implement this trait (I already have a way to do such things, which is used mainly to implement external traits (such as serde's Serialize and Deserialize for the generated wrapper).

Oh, well… I’ve missed the part where you mentioned macros before, in that case, right, the unreachable!() solution would not work straightforwardly.

Does the macro-generated code do anything else with that Clone bound besides calling the .clone() method? If not, I might have some additional ideas for possible approaches, feel free to ask.

I was referring to your code example where you wrote:

Then, trying to generalize the rule when methods would be “ignored” this way, I realized that actually ignoring the method (whatever exactly that means) can be problematic if the NonClonable type becomes clonable in the future.

There’s a distinction between the compiler

  1. knowing some type does implement Clone
  2. knowing some type does not implement Clone
  3. not knowing whether or not it implements Clone, either
    • because upstream code could add an implementation in the future, or
    • because the type contains a generic argument, and the Clone-ness depends on those arguments

If crate a defines NonClonable, and crate b depends on a and defines the Wrapper trait, the type ContainsNonClonable and the impl Wrapper for ContainsNonClonable, then crate b cannot rely on NonClonable not implementing Clone, because a future version of crate a might add an impl Clone for NonClonable. Then “ignoring” the method is problematic, whereas “pretending (locally) that NonClonable: Clone for reasoning about the code in the method” seems fine to me. The compiler, while compiling crate b considers the question whether NonClonable implements Clone in the 3rd case above.

On the other hand, if NonClonable is defined in crate b, too, then the reasoning can fall into the 2nd case “the compiler knows that NonClonable does not implement Clone”, in which case “ignoring the method” could be a viable strategy. In my mind, this should imply, one would not need to write the method at all, but writing it is not a problem either (in which case it would still be type-checked under the local assumption that NonClonable: Clone).

Here’s the idea already; adapting your playground from above

here’s what I’ve got: Rust Playground

Edit: or, using default-method-implementations instead of another trait e.g.: Rust Playground

2 Likes

No! Here is the code of the function in the current version of the crate. It will do the same in the next one. And yes, I would really love to hear your ideas for possible approaches!

Actually, both of the variants are not possible in my case. My crate only defines the Wrapper trait and provides a macro that any other crate can use to define a type that will implement the Wrapper trait. Basically, the basic usage looks like this:

use prae::Wrapper;

prae::define! {
    pub Text: String;
    // ...
}

If you expand the macro, you get this:

use prae::Wrapper;

pub struct Text(String);
impl prae::Wrapper for Text {
    type Inner = String;
    // all the other methods
}

One of these "other" methods is the mutate that requires Self::Inner: Clone. This makes it impossible to use prae::define! with an inner type that is not Clone.

This looks like dark magic but it seems like it will work! I will try to adapt this approach for my current API and let you know hot it goes. Thank you!

Thank you, it worked like a charm! I decided to go with the mutate / mutate_with variant because it's less verbose. Also, since the Wrapper trait is meant to be implemented by the provided macro, I decided to rename mutate_with into __mutate_with and add #[doc(hidden)] to it. This will make it almost invisible to the end user, which is desired.

1 Like

@steffahn I finished working on the update of my crate. Check it out if you're interested!

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.