Search code examples
genericsrusttraitsabstractiontrait-objects

How can a trait object take a trait with generic methods as an argument?


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>,
}

Solution

  • 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.