Updating a set based on matching generic type parameters

I'm working on some generic code for storing game delegates which implement a specific trait:

pub trait Delegate<TContext>: Debug + Sync + Send {
    fn context(&self) -> TContext;
}

Each delegate has an associated 'context' type with standard information about that value.

I would like to store these objects in a set data structure:

pub struct DelegateSet<TContext> {
    set: HashSet<TContext>,
}

impl<TContext> DelegateSet<TContext> {
    pub fn insert(&mut self, delegate: Box<dyn Delegate>) {
        // Somehow insert if delegate.context() matches TContext?
    }
}

But the problem I have is that I would like to be able to interact generically with this trait without always knowing the underlying type (i.e. by putting it into a Box<dyn Delegate>).

For example, I might want to iterate over a vector of these structs and pull out only the ones which match:

pub struct Battle {
    pub battle_context_set: DelegateSet<BattleContext>,
}

impl Battle {
    pub fn populate_game_context(&mut self, all: Vec<Box<dyn Delegate>>) {
        // Iterate over 'all' delegates and populate the BattleContext ones
        // into `Self::battle_context_set`.
    }
}

So what I'm mostly looking for here is a way to "hide" the generic type parameters sometimes, so that I can interact without knowing the real underlying type and do some runtime inspection on the values. Is there an idiomatic way to achieve this in Rust?

Do implementors of Delegate need to support more than one context? If so, I don't see a great way forward with the given design, as any form of type-erasing or generalizing the return type (e.g. using an enum) requires the implementor to choose one particular type of context to return.

For example:

impl Delegate<BattleContext> for Foo { /* ... */ }
impl Delegate<FraternizeContext> for Foo { /* ... */ }
// Type-erased version
impl Delegate<Box<dyn Any>> for Foo {
    // Do I return a `BattleContext` or a `FraternizeContext`?
    // Can't return both...
}

A not-great way if all contexts are known would be

trait DelegateSomeSubset {
    fn battle_context(&self) -> Option<BattleContext> { None }
    fn fraternize_context(&self) -> Option<FraternizeContext> { None }
    // etc
}

If only one context type per implementor need be supported, you probably wanted an associated type and not a type parameter.

trait Delegate {
    type Context;
    fn context(&self) -> Self::Context;
}

And you could have a type-erased version.

trait ErasedDelegate {
    // Alternatively, return an `enum` if all context types are known
    fn erased_context(&self) -> Box<dyn Any>;
    // Optional helper so you don't have to actually create the context if
    // it's not the type you need
    fn context_is(&self, id: TypeId) -> bool;
}

// (Can't have the blanket impl with an enum though)
impl<T, Context> ErasedDelegate for T
where
    T: ?Sized + Delegate<Context = Context>,
    Context: Any,
{
    fn erased_context(&self) -> Box<dyn Any> {
        Box::new(self.context())
    }
    
    fn context_is(&self, id: TypeId) -> bool {
        id == TypeId::of::<Context>()
    }
}

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.