1

So trait objects can't have methods with generics - that looks fine. But in this language the only ways to use abstraction mechanism are available through generics and trait objects. Which means that for each trait I have to decide beforehand if it can be used as an object at all and use dyn in there everywhere instead of impl. And all taken traits inside it must be made same way to support this. This feel very ugly. Can you suggest anything or tell me why it's designed this way?

fn main() {}

// some abstracted thing
trait Required {
    fn f(&mut self, simple: i32);
}

// this trait doesn't know that it's going to be used by DynTrait
// it just takes Required as an argument
// nothing special
trait UsedByDyn {
    // this generic method doesn't allow this trait to be dyn itself
    // no dyn here: we don't know about DynTrait in this scope
    fn f(&mut self, another: impl Required);
}

// this trait needs to use UsedByDyn as a function argument
trait DynTrait {
    // since UsedByDyn uses generic methods it can't be dyn itself
    // the trait `UsedByDyn` cannot be made into an object
    //fn f(&mut self, used: Box<dyn UsedByDyn>);

    // we can't use UsedByDyn without dyn either otherwise Holder can't use us as dyn
    // the trait `DynTrait` cannot be made into an object
    // fn f(&mut self, used: impl UsedByDyn);

    // how to use UsedByDyn here?
}

struct Holder {
    CanBeDyn: Box<dyn DynTrait>,
}
Vlad
  • 3,001
  • 1
  • 22
  • 52

2 Answers2

6

Which means that for each trait I have to decide beforehand if it can be used as an object at all and use dyn in there everywhere instead of impl.

You can do that, but fortunately it's not the only option.

You can also write your traits as you normally would, using generics where appropriate. If/when you need trait objects, define a new object-safe trait that you use locally, and that exposes the subset of the API you actually need in that place.

For example, let's say you have or use a non-object-safe trait:

trait Serialize {
    /// Serialize self to the given IO sink
    fn serialize(&self, sink: &mut impl io::Write);
}

That trait is not usable as a trait object because it (presumably to ensure maximum efficiency) has a generic method. But that needn't stop your code from using trait objects to access the functionality of the trait. Say you need to box Serialize values in order to hold them in a vector, which you will save into a file en masse:

// won't compile
struct Pool {
    objs: Vec<Box<dyn Serialize>>,
}

impl Pool {
    fn add(&mut self, obj: impl Serialize + 'static) {
        self.objs.push(Box::new(obj) as Box<dyn Serialize>);
    }

    fn save(&self, file: &Path) -> io::Result<()> {
        let mut file = io::BufWriter::new(std::fs::File::create(file)?);
        for obj in self.objs.iter() {
            obj.serialize(&mut file);
        }
        Ok(())
    }
}

The above doesn't compile because Serialize is not object safe. But - you can easily define a new object-safe trait that fulfills the needs of Pool:

// object-safe trait, Pool's implementation detail
trait SerializeFile {
    fn serialize(&self, sink: &mut io::BufWriter<std::fs::File>);
}

// Implement `SerializeFile` for any T that implements Serialize
impl<T> SerializeFile for T
where
    T: Serialize,
{
    fn serialize(&self, sink: &mut io::BufWriter<std::fs::File>) {
        // here we can access `Serialize` because `T` is a concrete type
        Serialize::serialize(self, sink);
    }
}

Now Pool pretty much just works, using dyn SerializeFile (playground):

struct Pool {
    objs: Vec<Box<dyn SerializeFile>>,
}

impl Pool {
    fn add(&mut self, obj: impl Serialize + 'static) {
        self.objs.push(Box::new(obj) as Box<dyn SerializeFile>);
    }

    // save() defined the same as before
    ...
}

Defining a separate object-safe trait may seem like unnecessary work - if the original trait is simple enough, you can certainly make it object-safe to begin with. But some traits are either too general or too performance-oriented to be made object-safe from the get-go, and in that case it's good to remember that it's ok to keep them generic. When you do need an object-safe version, it will typically be for a concrete task where a custom object-safe trait implemented in terms of the original trait will do the job.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • In your `SerializeFile` trait you use the concrete struct `BufWriter` but the original trait (`Serialize`) argument was `Write` which is trait. This is cheaty. What if you don't know that the concrete type will be `BufWriter`? – Vlad Nov 21 '20 at 22:22
  • Also what if you need to give `Serialize` instances back at some point? How would you convert `SerializeFile` back to `Serialize`? – Vlad Nov 21 '20 at 22:35
  • @Vlad I understand why it feels cheaty, but it makes a different point. `SerializeFile` is `Serialize` applied to files, a _local_ and more narrow trait tailored to the needs of `Pool`. Sure, you can go one step further and make `SerializeFile::serialize` [accept `&mut dyn io::Write`](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=cbd37113a709df995afe8ce97aae6c8b), but that could also be considered cheating because `io::Write` happens to be object-safe, and in your original example the trait accepted as argument wasn't. – user4815162342 Nov 21 '20 at 22:40
  • The answer argues that it's ok to create a new trait with a _different_ interface that sacrifices some of the generality of the original trait for object-safety, and achieves it (and this is the most important part) *without modifying the original trait*. Because of that, you don't need to worry about a trait not being object-safe _in advance_ because you can use it to create an object-safe one later down the line. – user4815162342 Nov 21 '20 at 22:40
  • @vlad *Also what if you need to give Serialize instances back at some point? How would you convert SerializeFile back to Serialize* - I'm not sure I understand this - `Serialize` is a trait, there are no "Serialize instances". Maybe it would help if you edited the question to provide more details on your actual use case. – user4815162342 Nov 21 '20 at 22:42
  • Your code accepts `impl Serialize`, makes coercion into `dyn SerializeFile` and stores the result inside struct. What if you need to give the original instance back? Ah, ok, after some time spent I can't figure out an example where this and storing trait objects both can be needed at the same time. – Vlad Nov 21 '20 at 23:06
  • @Vlad You might get a reference to the original instance for a particular type using `Any`, but it will require knowing or guessing the concrete type, you can't revert to `impl Serialize` because that's type-erased. – user4815162342 Nov 21 '20 at 23:21
  • 1
    @Vlad See also [this answer](https://stackoverflow.com/a/64840069/1600898) for a practical example of applying an object-unsafe std trait (`std::hash::Hash`) to create a type-erased wrapper that implements `Hash` and can be used as hash table key. It also defines a custom trait that _is_ object-safe and that calculates the hash in its blanket impl, where we do have the concrete type and can invoke `::hash()` even though `Hash` is not object-safe. Perhaps that's a better demonstration of the concept. – user4815162342 Nov 21 '20 at 23:25
  • I've written my own version the doesn't require being cheaty, see the other answer. – Vlad Nov 22 '20 at 13:32
1

I used the @user4815162342's answer but made my own version that doesn't require replacing a non-object-friendly trait with a concrete type.

struct Holder {
    dyn_traits: Vec<Box<dyn DynTrait>>,
}

// this trait doesn't know that it's going to be used by DynTrait
// it just takes ObjectFriendly as an argument
// nothing special
trait ObjectUnfriendly {
    // this generic method doesn't allow this trait to be dyn itself
    // no dyn here: we don't know about DynTrait in this scope
    fn f(&mut self, another: &impl ObjectFriendly);
    fn f2(&mut self, another: &mut impl ObjectFriendly);
    fn f3(&mut self, another: impl ObjectFriendly);
}

trait ObjectFriendly {
    fn f(&mut self, simple: i32);
    fn f2(&self, simple: i32);
}

// this trait needs to use the trait above as a function argument
trait DynTrait {
    // since that trait uses generic methods it can't be dyn itself
    // the trait cannot be made into an object
    //fn f(&mut self, used: Box<dyn ObjectUnfriendly>);

    // we can't use that trait without dyn either otherwise Holder can't use us as dyn
    // the trait `DynTrait` cannot be made into an object
    // fn f(&mut self, used: impl ObjectUnfriendly);

    // how to use ObjectUnfriendly here?
    // we use our own extension trait that is object-friendly
    fn f(&mut self, used: dyn NowObjectFriendly);
}

// our own object-friendly version
trait NowObjectFriendly {
    // if arguments are ObjectFriendly - we are lucky
    fn f(&mut self, another: &dyn ObjectFriendly);
    fn f2(&mut self, another: &mut dyn ObjectFriendly);
    fn f3(&mut self, another: Box<dyn ObjectFriendly>);

    // if not - we can just accept the specific struct we need
    // fn f3(&mut self, another: SomeImpl);

    // or do the same thing by making an extension trait
    // fn f3(&mut self, another: Box<dyn ObjectFriendly2Ex>);
}

// delegate implementation
impl<T: ObjectUnfriendly> NowObjectFriendly for T {
    fn f(&mut self, another: &dyn ObjectFriendly) {
        self.f(&ObjectFriendly2AsImpl(another));
    }

    fn f2(&mut self, another: &mut dyn ObjectFriendly) {
        self.f2(&mut ObjectFriendly2AsImpl(another));
    }

    fn f3(&mut self, another: Box<dyn ObjectFriendly>) {
        self.f3(ObjectFriendly2AsImpl(another));
    }

    // if not object friendly - we can just accept the specific struct we need
    // fn f3(&mut self, another: SomeImpl) {
    //     SomeImpl::f3(self, another);
    // }

    // or do the same thing for that trait by making another extension trait
    // fn f3(&mut self, another: Box<dyn ObjectFriendly2Ex>) {
    //     self.f3(another);
    // }
}

// for this delegation to work
// we need to make it convertible to impl

// can't implement foreign traits on foreign types
struct ObjectFriendly2AsImpl<T>(T);

impl ObjectFriendly for ObjectFriendly2AsImpl<&dyn ObjectFriendly> {
    fn f(&mut self, simple: i32) {
        unreachable!()
    }

    fn f2(&self, simple: i32) {
        (*self.0).f2(simple)
    }
}

impl ObjectFriendly for ObjectFriendly2AsImpl<&mut dyn ObjectFriendly> {
    fn f(&mut self, simple: i32) {
        (*self.0).f(simple)
    }

    fn f2(&self, simple: i32) {
        (*self.0).f2(simple)
    }
}

impl ObjectFriendly for ObjectFriendly2AsImpl<Box<dyn ObjectFriendly>> {
    fn f(&mut self, simple: i32) {
        (*self.0).f(simple)
    }

    fn f2(&self, simple: i32) {
        (*self.0).f2(simple)
    }
}

If there is a macro for this or more lightweight implementation please comment.

Vlad
  • 3,001
  • 1
  • 22
  • 52