Provide default fn in a subtrait for the parent trait

Hi all -- I'm very new to Rust and still trying to wrap my brain around The Rust Way, so if there's a better approach, please let me know.

I have a set of objects that can be written to anything with the Write trait, so I've defined a Writeable trait that requires them to expose a write_bytes() function.

trait Writeable {
    fn write_bytes<T: Write>(&self, out: &mut T) -> Result<usize>;
}

For some of these objects--the simpler ones--it's possible to provide a default implementation of write_bytes(), so I've created a subtrait for those simple objects:

trait BatchOneByteOption: Writeable {
    const ID: BatchOptionId;
    fn id() -> BatchOptionId {
        Self::ID
    }
    fn val(&self) -> u8;
    fn write_bytes<T: Write>(&self, out: &mut T) -> Result<usize> {
        out.write(&[Self::id() as u8, self.val()])
    }
}

but the default implementation in BatchOneByteOption doesn't seem to satisfy the requirement Writeable imposes. This:

struct BatchEndOption {}
impl BatchOneByteOption for BatchEndOption {
    const ID: BatchOptionId = BatchOptionId::BatchEnd;
}

yields this error:

the trait bound `BatchEndOption: Writeable` is not satisfied
the trait `Writeable` is not implemented for `BatchEndOption`rustc(E0277)

What am I doing wrong, or is there a better way?

First, subtraits don't specialize their supertrait's methods; that is, if I have

trait Super { fn foo(&self); }
trait Sub: Super { fn foo(&self); }

The two methods are distinct. You would have to implement both in order to implement Sub, not just one. And you don't want this arrangement anyway, because if you do and try to call foo as a method...

    unit.foo();

You'll get an error[E0034]: multiple applicable items in scope as it is ambiguous which method should be called. Playground. (You can disambiguate like the error says, but it's unlikely you really want this.)

Specialization is planned, but it is not yet stable.

Second, related to the first: implementing a subtrait doesn't implement the supertrait. That's what your error was about; you only implemented the subtrait.


What you're trying to do here is implement the supertrait Writable for everything that implements the subtrait BatchOneByteOption. You can do so like this:

// n.b. `write_bytes` has been removed from the `BatchOneByteOption` trait
impl<Bobo: BatchOneByteOption> Writeable for Bobo {
    fn write_bytes<T: Write>(&self, out: &mut T) -> io::Result<usize> {
        out.write(&[Self::id() as u8, self.val()])
    }
}

Playground.

One forward-looking limitation to keep in mind is that you only have one generic implementation like this, because if you add another:

trait SomeOtherApproach: Writable {
    fn foo(&self) -> io::Result<usize>;
}

impl<Soa: SomeOtherApproach> Writeable for Soa {
    fn write_bytes<T: Write>(&self, _out: &mut T) -> io::Result<usize> {
        self.foo()
    }
}

It would open the door to the possibility of multiple implementations of the same trait for a single type (if that type implemented both BatchOneByteOption and SomeOtherApproach). Playground.

But this may not be an issue for your particular use case.

2 Likes

This second reply is on a less technical and more intuition-based level.

When you have a subtrait/supertrait relationship, it's usually the case that the subtrait needs the supertrait's functionality. That is, implementations of the subtrait can and need to utlize the supertrait. You don't even have to declare this dependency on the implementation, because once you've declare the supertrait relationship, the supertrait is an implied bound everywhere the subtrait is a bound.

But in your use case, you don't need to be Writeable in order to be BatchOneByteOption, even though there's a clear way to implement the former given the latter. Moreover, it's not clear to me that BatchOneByteOption is even a useful bound on its own (I'm not saying it isn't, just that I can't tell if it is from the example). If you would always prefer:

fn use_this_stuff<T: Writeable>(t: T) { ... }

Over:

fn use_this_stuff<T: BatchOneByteOption>(t: T) { ... }
// Implied: T: Writeable too

Then perhaps the subtrait/supertrait relationship is unnecessary. It is true that, with the generic implementation, BatchOneByteOption can do Writeable and more. But from the perspective of a Writeable consumer (interested in writing bytes), BatchOneByteOption doesn't seem to add much related capability. And you can still have the generic implementation of Writeable for BatchOneByteOption implementers without the subtrait/supertrait relationship.


I suspect there's a deeper mindset clash between object inheritance and and trait (interface) inheritance at play here, but this is as far as I got in thinking it through. I'm curious if anyone else has something to say about it.

1 Like

Thank you, that makes a ton of sense and clarifies how traits work in Rust. Thanks for your help!

1 Like