Polymorphic implementation related to present or missing trait

Hello,

I'm playing with code, which simplified looks like this:

use std::io::Cursor;
use seek_test::*;

fn main() {

 let w1: Vec<u8> = Vec::new();
 let w2 = Cursor::new(Vec::<u8>::new());

 let mut writer1 = Writer::new_unseekable(w1);
 let mut writer2 = Writer::new(w2);

 assert!(!writer1.check_can_seek());
 assert!(writer2.check_can_seek());

 writer1.write(b"abc").unwrap();
 writer2.write(b"cde").unwrap();

 assert_eq!(3, writer1.position().unwrap());
 assert_eq!(3, writer2.position().unwrap());

// this cannot be achieved with trait object, 
// because there is no way to own back type
// turned to trait object ?
let returned_w1 = writer1.unwrap();
let returned_w2 = writer2.unwrap().into_inner();

assert_eq!(b"abc", &returned_w1[..]);
assert_eq!(b"cde", &returned_w2[..]);

}

So basically I have type Writer, which can contain other type than implements Write. Now inner type may or may not implemented Seek trait. I want to work with both, and for those that did not have Seek capability to provide some workaround to get current position in stream (just by calculating written bytes). Also internally Writer should behave slightly differently if inner can seek or not .

So I tried to implement - create a trait that abstracts common behaviour of inner stream, then Wrapper type for seekable and nonseekable types, which I put around true inner type and have impl to distinguish, which wrapper to use.
The code is here https://gist.github.com/izderadicka/f84d828b8fde48fb7e37d24324e5f38d#file-lib-rs

My problem with this implementation is:
a) It's a lot of gluing code to make it work, so it rather intensive comparing to quite simple purpose
b) All supporting traits and structs must be public, because they leak into main type, which kind of breaks encapsulation

So I wonder if this approach is optional, if there could be easier, more elegant solution, or at least some partial improvements. Or maybe completely different approach.

I of course tried trait objects - see this code https://gist.github.com/izderadicka/f84d828b8fde48fb7e37d24324e5f38d#file-lib2-rs

It nicely hides implementation details, cause all machinery now can be private to module, but has couple of issues:
a) At the end I'd need to retain back full ownership of inner stream, with trait objects it does not seem to be available, as once turned to trait object I cannot get back ownership of original object (only reference via downcasting). Or am I missing something?
b) I would kind of prefer static dispatch in this case

So any ideas, comments, I'd like to see anything, thanks.

Hope this is helpful. playground I got a working version where you pass a closure in to access the dyn MaySeek as Vec or Cursor depending on what you are trying to do with it maybe this will help.

pub fn un_dyn_ify<F>(&mut self, mut f: F)
    where
        F: FnMut(&mut W) -> () // if you want to return something
          // lifetimes may get tricky
    {
        f(self.inner.as_inner_mut())
    }
1 Like

enum is used in such cases:

enum WriterFlavor<W> {
    Seekable(SeekableWriter<W>),
    Unseekable(UnseekableWriter<W>),
}

// Usually, the enum is wrapped further in a struct
// to protect privacy from public API.
struct Writer<W> {
    flavor: WriterFlavor<W>,
}

Yes, there are some boilerplate wrapping / unwrapping but it is the most common way to do in the current Rust.

The method Writer<W> -> W is called into_inner commonly.

Thanks,

I actually tried to use enum first without trait, which looked like more straightforward approach, but hit this problem:
at some moment I need to distinguish behavior for wrapper stream - either it's Write only or it's Write+Seek. However I cannot do two impls of same function for <T:Write> and <T:Write+Seek> because it won't compile - as it will be duplicate implementation of same function. So the only solution was to have two separate types implementing same trait.

into_inner - yes this would be better name, I just used unwrap because it was in the original interface I was extending.

Thanks,
yes this works in this case, but probably I did not make myself clear, I really need to get back ownership of wrapper stream - due to restrictions of existing interface in the real project ( not just this simplified example). But thinking about it it should be possible - having wrapped stream in option I can then take it and return it. I'll try this.

1 Like

@DevinR528 - I tried with option and can return ownership of the inner stream - the my implementation is here https://gist.github.com/izderadicka/f84d828b8fde48fb7e37d24324e5f38d#file-lib3-rs

One novel revelation was that inside impl block I can futher limit type parameter defined on impl for individual functions, quite cool.

So now solution with dynamic dispatch can work for me, it hides implemetation details, which is great, one small disadvantage is that it's ehhmm dynamic dispatch. But I guess I really cannot achieve same with static dispatch because I cannot use enum in this case ( as explained in previous reply there is no way to have two different impls for one enum for both <W:Write> and <W:Write+Seek>, and if I use helper traits and separate types then these inevitably leek into mod interface.

1 Like

Hopefully this helps (playground):

type IntoSeek<W> =
    for<'a> fn(&'a mut W) -> &'a mut dyn Seek;

enum Flavor<W> {
    Seekable(IntoSeek<W>),
    Unseekable(u64)
}

pub struct Writer<W> {
    inner: W,
    flavor: Flavor<W>
}

this is not entirely true if you are willing to use nightly features #![feature(specialization)] will allow you to write impl's based on the bounds of your generic parameter.
playground example
here is some more info on specialization. I think you could use it for the WrapperSeekable and the Writer impls maybe? The limitation of specialization is that it can only be applied once meaning:

impl<T> Foo for Bar<T>
impl<T: SomeTrait> Foo for Bar<T>
// not allowed
impl<T: AnotherTrait> Foo for Bar<T>

I see. My solution is storing seek function pointer for the Seekable flavor. https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=0c50e83ae8ff56e95778017398878ad0.

Thanks,
specialization is super-cool, I did not know that it's coming, it would be exactly what I need. Right now I'm targeting stable release, but this would be great tool for my future projects (coming from OOP background overriding methods is first trick I can think of).

1 Like

Thanks, this is nice solution, which I would probably never think of.
I guess with each seek there will be small overhead with creating trait object (vtable to inner 3 or so methods).
I assume having directly &mut dyn Seek in Writer struct will not be possible because then I cannot use inner elsewhere, because it'll be uniquelly borrowed already.

@pcpthm Thanks a mil, this looks like best solution (almost magical :slight_smile: It definitely solves all my current problems, code is nice and lean. I did not realize that something like this would be possible. For reference here is my complete example - https://gist.github.com/izderadicka/f84d828b8fde48fb7e37d24324e5f38d#file-lib4-rs

@pcpthm @DevinR528 @jp-beaumont I just want to thank again everybody for their great ideas. I learned a lot from you comments and code. If you wonder what was the real case for my questions see https://github.com/mvdnes/zip-rs/pull/114 - proposed solution fits there perfectly!

1 Like