Is creating and implementing a trait really neccessary here?

I'm trying to make a function that accepts a string that represents either a preset sound or a path, and then plays it using rodio.
This:

let data = match sound {
    "alarm" => Cursor::new(sounds::ALARM),
    path => BufReader::new(File::open(path).unwrap()),
};

Doesn't compile: error[E0308]: match arms have incompatible types. I figured I needed to use an explicit type and box:

let data: Box<dyn std::io::Read + std::io::Seek + Send + Sync> = match sound {
    "alarm" => Box::new(Cursor::new(sounds::ALARM)),
    path => Box::new(BufReader::new(File::open(path).unwrap())),
};

But that also doesn't compile: error[E0225]: only auto traits can be used as additional traits in a trait object
Then I created a trait, added impls and did everything the compiler asked, and it finally compiled:

trait Data: std::io::Read + std::io::Seek + Send + Sync {}

impl<T: Sync + Send + AsRef<[u8]>> Data for Cursor<T> {}
impl<T: Sync + Send + std::io::Seek + std::io::Read> Data for BufReader<T> {}

pub fn play_sound(sound: &str) {
    let (_stream, stream_handle) = OutputStream::try_default().unwrap();

    let data: Box<dyn Data> = match sound {
        "alarm" => Box::new(Cursor::new(sounds::ALARM)),
        path => Box::new(BufReader::new(File::open(path).unwrap())),
    };

    let source = Decoder::new(data).unwrap();

    stream_handle.play_raw(source.convert_samples()).unwrap();
}

Is this really all necessary for something so simple?

As of right now, yes. Trait objects can only consist of one object-safe base trait plus auto-traits (read about it in the reference). I think trait objects with multiple base traits is currently a pre-RFC feature (I found this issue):

1 Like

Thanks for the answer!
That's unfortunate... This is kind of what scares me away from Rust (as I'm used to Python), this amount of boilerplate for something that's not very rare (at least not for me) is insane.

Perhaps there is an easier way of accomplishing your goal? If you know all the possible options and they are not in foreign crates / user code, you can just use an enum instead.

1 Like

How would I use an enum for this? Would I need to use an enum all the way through to the Decoder?

I think the simplest way to do this would be to duplicate the Decoder::new and stream_handle.play_raw(source.convert_sample( calls to both match arms. It's very WET, but it might be better than the above unreadable boilerplate.

Rust is usually low-boilerplate. In this case, the "insane" amount of code you are complaining about is three lines. Frankly, calling this "insane" doesn't seem to be warranted at all, and it's pretty rude.

But it could still be reduced to two if you make the impl generic:

impl<T: Read + Seek + Send + Sync> Data for T {}

The reason why this isn't yet done automatically is because the best implementation strategy is not at all obvious. For example, should every trait object suddenly be larger when there are more than one traits in it? You might be used to multiple inheritance in Python, but the language has burnt in several choices (and their corresponding problems) into the language (e.g. do you really know all the method resolution order rules by heart?).

Well, yes. Instead of Box<dyn Trait> you would use use Enum everywhere and add the methods you need to Enum (choosing names that make sense in your context ofc).

Rust is usually low-boilerplate. In this case, the "insane" amount of code you are complaining about is three lines. Frankly, calling this "insane" doesn't seem to be warranted at all, and it's pretty rude.

With "insane boilerplate" here I don't mean it's lots of code, but that it's very unclear what it does. I just want to know how the play_sound function works, why do I need to read through all this trait and impl stuff?
Insane is a too strong word, but I don't think this specific case is low-boilerplate at all. This is my opinion as a beginner that has always programmed in Python, so that opinion will probably change :sweat_smile:

You might be used to multiple inheritance in Python, but the language has burnt in several choices (and their corresponding problems) into the language (e.g. do you really know all the method resolution order rules by heart?).

With "I'm used to Python" I didn't mean its inheritance system, but the dynamic typing. In Python I would just say

data: BytesIO
if sound == 'alarm':
    data = ...

This makes it a lot more readable, thanks.

I think Either in either - Rust should support Read and Seek. Haven't tested if it works here, but if so, it's less boilerplate (avoiding the trait definition and impl, and even avoiding the type signature) and less boxing.

1 Like

Either works, and reduces boilerplate by a lot, but feels like a workaround; what if I have three possibilities? (I don't think that's very common, but the trait works for that, and Either doesn't)

Well… if it’s just 3 (or not all that many), you can still reasonably do things like Either<X, Either<Y, Z>> :innocent:

So Left(foo), Right(Left(bar)) and Right(Right(baz)) for example. Binary-code your lefts and rights if you like ^^[1]


  1. For 8, that's be Left(Left(Left(x1))), Left(Left(Right(x2))), Left(Right(Left(x3)))…, Right(Right(Right(x8))) ↩︎

I think I would rather make my own enum than do that, but it works.

Thanks everyone! I think I'll stick to Either for now, when there are two cases.

I've seen (multiple times) the desire to make defining ad-hoc enums that implement a (set of) trait(s) easier. And I wouldn't be surprised if we do end up getting anything like that one day in the future. E.g. some pseudo-syntax for what such a language feature might possibly look like:

pub fn play_sound(sound: &str) {
    let (_stream, stream_handle) = OutputStream::try_default().unwrap();

    let data: enum impl std::io::Read + std::io::Seek = match sound {
        "alarm" => enum(Cursor::new(sounds::ALARM)),
        path => enum(BufReader::new(File::open(path).unwrap())),
    };

    let source = Decoder::new(data).unwrap();

    stream_handle.play_raw(source.convert_samples()).unwrap();
}

The obvious advantages over Either would, for such a use-case, be that

  • more traits are supported than just the ones that Either supports
  • more than 2 variants are supported without any loss in ergonomics
1 Like

Yet another future possibility that comes to mind is generic closures; this kind of approach would increase the amount of generated code, whilst eliminating any need for delegation or dispatching for the Read/Seek.

pub fn play_sound(sound: &str) {
    let (_stream, stream_handle) = OutputStream::try_default().unwrap();

    let handle_data = |data: impl std::io::Read + std::io::Seek| {
        let source = Decoder::new(data).unwrap();

        stream_handle.play_raw(source.convert_samples()).unwrap();
    };
    match sound {
        "alarm" => handle_data(Cursor::new(sounds::ALARM)),
        path => handle_data(BufReader::new(File::open(path).unwrap())),
    };
}

Of course the same logic can also, already, be written using functions (though the boilerplate then grows since stream_handle could no longer conveniently be captured), or macros, or code duplication.

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.