genericsrustdependency-injectionvisitor-pattern

Defining and blanket implementing a common trait to avoid dependencies in Rust


High level problem statement

I have various generic containers defined in many different crates using a common declarative macro. Each looks something like:

struct Container1<T> {
    a: T,
    b: T,
}

On the other hand, I have a separate crate model with a trait ModelTree that I want to implement on all the generic containers, when their type parameters implement some other trait Model:

trait ModelTree {
    fn integrate_model_tree(self, context: Context);
}

Ideally this would be

impl<T: Model> ModelTree for Container1<T> {
    ...
}

[...and similarly for other containers...]

However, the model crate cannot depend directly on all the crates that define the containers (there are many), and the crates that define the containers also cannot depend on the model crate.

To get around this, at a high level I thought to define a common crate common that defines a trait that is "similar to" ModelTree, have all the containers (e.g. Container1) implement this common trait, and then implement ModelTree generically for anything that implements the common trait. However, I keep getting unconstrained type parameter whenever I make an attempt to do this.

Concrete attempt

Here's concretely what I tried to do in common. This playground link has the full MRE. Define a common visitor trait:

trait Visitor {
    type ItemType;

    fn visit(&mut self, item: Self::ItemType);
}

Define a "visitable" trait, AcceptVisitor:

trait AcceptVisitor<V: Visitor<ItemType = Self::ItemType>> {
    type ItemType;

    fn accept_visitor(self, visitor: V);
}

Implement AcceptVisitor for all generic containers:

impl<T, V: Visitor<ItemType = T>> AcceptVisitor<V> for Container1<T> {
    type ItemType = T;

    fn accept_visitor(self, mut visitor: V) {
        visitor.visit(self.a);
        visitor.visit(self.b);
    }
}

Add a visitor shim in the model crate:

struct ModelVisitor<M: Model> {
    context: Context,
    phantom: core::marker::PhantomData<M>,
}

impl<M: Model> Visitor for ModelVisitor<M> {
    type ItemType = M;
    
    fn visit(&mut self, item: M) {
        self.context.add_model(item);
    }
}

Finally, implement ModelTree for all types that implement AcceptVisitor:

impl<
        M: Model,
        V: Visitor<ItemType = M>,
        A: AcceptVisitor<ModelVisitor<M>, ItemType = M>,
    > ModelTree for A
{
    fn integrate_model_tree(self, context: Context) {
        let visitor = ModelVisitor { context, phantom: core::marker::PhantomData };
        self.accept_visitor(visitor);
    }
}

This is where I get the unconstrained type parameter error for both M and V.

Question

Is there some way to accomplish my high-level problem statement? Am I going down the wrong path with my concrete attempt, or is there a way to tweak it to make it work? Thanks!


Solution

  • This smells hard like a XY problem, but if that really is what you want to do, you can move the Visitor generic parameter from the trait to the accept_visitor() method. This is also more correct, since the implementation doesn't rely on a single visitor.

    trait AcceptVisitor {
        type ItemType;
    
        fn accept_visitor<V: Visitor<ItemType = Self::ItemType>>(self, visitor: V);
    }
    
    impl<T> AcceptVisitor for Container1<T> {
        type ItemType = T;
    
        fn accept_visitor<V: Visitor<ItemType = T>>(self, mut visitor: V) {
            visitor.visit(self.a);
            visitor.visit(self.b);
        }
    }
    
    impl<M: Model, A: AcceptVisitor<ItemType = M>> ModelTree for A {
        fn integrate_model_tree(self, context: Context) {
            let visitor = ModelVisitor {
                context,
                phantom: core::marker::PhantomData,
            };
            self.accept_visitor(visitor);
        }
    }