[Solved] Calling a default subtrait method from the supertrait default method

I have a trait Super that bounds a trait Sub. Both Super and Sub have a method foo(), but Super has only the signature of foo(), while Sub has a default implementation of foo(). The supertrait has a Super::bar() that calls foo() in it.
I wan to impl these traits for a struct Blah, such that when I call Super::bar() on the instance of the struct, the more specific Sub::foo() implementation from the trait Sub is called. Here is the code sketch:

trait Super  {

  fn foo(&self) -> u32; //default implementation

  fn bar(&self) -> u32 {
    self.foo()
  }
}
     
trait Sub : Super  {
  fn foo(&self) -> u32 {
    1
  }
}

struct Blah {}


impl Super for Blah {} //Compile Error missing `foo` in implementation
impl Sub for Blah {} //because I want the foo() from this impl


fn main() {
  let blah = Blah{};
  blah.bar(); //I would like to get 1 here
}

How can I achieve this?

You could turn it around, have a trait Foo that providers the method foo and have it bound Super (or however you would call it).

That would solve the Problem at Hand, if your actual problem isn't solved this way please post the real problem :slight_smile:

the idea was that trait Sub has a more specific implementation Sub::foo than Super::foo, (and Sub has some other specific methods Sub::baz ). Other structs can impl Super without implementing Sub and without those Sub-specific other methods Sub::baz() that they don't need. If I switch them around, can I still achieve that?

As far as I know thats currently not possible directly.
You might be interested in this tracking issue: https://github.com/rust-lang/rust/issues/31844

If you implement Super and Sub by hand for all types, you could just write their specific implementations to have the behavior you want.
If you have a lot of functions you want to just delegate in case of types that implement Super there are some crates that provide macros for this (delegate would be my keyword).

Hope this helps

Keep in mind that Rust is not an object oriented language.

3 Likes

Remove:

/// You would have written this for each concrete implementor of `Sub`,
/// such as `Blah`
impl Super for Blah {}

and instead write:

/// One impl to rule them all
/// so we can afford being explicit about the "delegation"
impl<T : Sub> Super for T {
    #[inline]
    fn foo (self: &'_ Self)
      -> u32
    {
        Sub::foo(self)
    }
}
1 Like

@Yandros This seems to be the right approach, I marked it as solved!
The only remaining question is, say trait Suber has some method signatures that need to be implemented for all structs. When I implemented Super for all structs directly, I could use self.field from each struct in that impl. Now since Super is implemented for a generic type T bound to Sub, I cannot use self.field. Therefore I have to duplicate these basic methods from Super into Sub (as signatures) and then when I impl Sub for Blah for my structs I can use self.field.
But logically, these basic methods belong to Super, not to Sub, because they are impled for all structs. Is there any way around that?

Yes, my answer was not complete :wink:

Indeed, my solution relies on all of Super methods' implementations "delegating" to Sub, which is indeed far from always being the case.

The short answer then, relies on an unstable / in-the-works feature: specialization. Mainly the default impl construct (to be unserstood as partial impl), and whose purpose is to provide default implementations for some of the methods:

#![feature(specialization)]

trait Super {
    fn foo (self: &'_ Self)
      -> u32
    ;
    fn bar (self: &'_ Self)
    ;
}

trait Sub {
    fn foo (self: &'_ Self)
      -> u32
    ;
}

default impl<T : Sub> Super for T {
    #[inline]
    fn foo (self: &'_ Self)
      -> u32
    {
        Sub::foo(self)
    }
}

impl Sub for Blah {
    fn foo (self: &'_ Self)
      -> u32
    {
        ... // can access `Blah`'s fields and inherent methods
    }
}
/// Given that `Blah : Sub`, we don't need to provide an implementation for `foo`.
impl Super for Blah {
    fn bar (self: &'_ Self)
    {
        ... // ditto
    }
}

Now, this only works on nightly Rust, which is a bummer. Does that mean it cannot be done on stable Rust? Nope, it can be done, but it is a tad more cumbersome, since it involves splitting Super's "defaultable" and non-"defaultable" methods, by using a helper trait:

trait Super : SuperFoo {
    /* From SuperFoo: */
    // fn foo (self: &'_ Self)
    //  -> u32
    // ;
    fn bar (self: &'_ Self)
    ;
}
// where
trait SuperFoo {
    fn foo (self: &'_ Self)
      -> u32
    ;
}

trait Sub {
    fn foo (self: &'_ Self)
      -> u32
    ;
}

impl<T : Sub> SuperFoo for T {
    #[inline]
    fn foo (self: &'_ Self)
      -> u32
    {
        Sub::foo(self)
    }
}


impl Sub for Blah {
    fn foo (self: &'_ Self)
      -> u32
    {
        ... // ditto
    }
}
/// Given that `Blah : Sub`, we don't need to provide an implementation for `foo`
/// (through an `impl SuperFoo for Blah`).
impl Super for Blah {
    fn foo2 (self: &'_ Self)
      -> u32
    {
        ... // can access `Blah`'s fields and inherent methods
    }
}
  • The idea is to perform this "trait split" in order for one of the traits (here, SplitFoo), to match the situation of my original answer.
1 Like

@Yandros the specialization feature is ideal. Is it guaranteed that it will eventually make it into the stable Rust? Is there such guarantee for all nightly features in general?

No, although I think it is likely that specialization will eventually make its way.

2 Likes

I'm pretty confident that default impl will; it's a pretty innocent feature / ergonomics improvement.

But impl ... { default fn ... }, on the other hand, is still on the works; not only has it led to some unsound borrow-checker bugs, it can also "break" some properties of the language, such as parametricity:

Out-of-topic rant showing an example

Imagine a function taking an impl for<T> Fn(T) -> T.
Granted, we don't have such higher-rank signatures in the language yet, but that can be modelled quite easily as:

trait Trait {
    fn f<T> (_: T) -> T;
}

fn example<U : Trait> ()
{
    // U::f "is" a `for<T> fn(T) -> T`
}

then, without specialization, the following function is sound:

use ::std::{mem, rc::Rc};

fn foo<U : Trait> ()
{
    let ft = Rc::new(42);
    let at_ft: &'static i32 = unsafe {
        let ptr: *const i32 = &*ft;
        mem::forget(U::f(ft));
        &*ptr
    };
    assert_eq!(*at_ft, 42);
}

Indeed:

  1. When U::f returns its input (e.g., |x| x), then the function is sound (the unsafe block is basically Rc::leak).

  2. Given the higher-kinded-ness of U::f and how unbounded it is, without specialization, there are only two ways an implementation can match the signature: either it diverges (in which case the unsafe {} code is never reached, so no harm in that case), or it returns the input as is, thus going back to 1.

But with impl ... { default fn ... } specialization, foo is not sound:

This is not to say that impl ... { default fn ... } specialization is bad per se, just that it is hard to miss all the implications of having it (c.f. crates relying on generative lifetimes for soundness, such as ::indexing)

2 Likes

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