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):
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.
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
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
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.
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)
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
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.