Use associated type with trait bound as generic member of struct

I've tried so many different ways to do this I lost count. :laughing:

I have a struct that needs to store a single instance of a trait object. This trait object is created from another trait. Both traits have associated error types meaning I can't easily Box them.

The compiler tells me

error[E0308]: mismatched types
  --> src/main.rs:53:13
   |
48 | impl<S: Stored> Storer<S> {
   |      - expected this type parameter
...
53 |             stored
   |             ^^^^^^ expected type parameter `S`, found associated type
   |
   = note: expected type parameter `S`
             found associated type `<C as Creator>::Storer`
   = note: you might be missing a type parameter or trait bound

Minimal example below, full example on Rust Playground

// First trait - this gets called to create the `Stored` trait object
trait Creator {
    type Error: std::error::Error + Send + Sync + 'static;
    type Storer: Stored; 

    // Is a type parameter "more" correct here?
    fn create(&mut self) -> Result<Self::Storer, Self::Error>;
}

// This is the trait object I need to store
trait Stored {
    type Error: std::error::Error + Send + Sync + 'static;
    // other methods here
}

// 2 struct implement the above traits here

// --- The struct that needs to store the trait object
struct Storer<S: Stored> {
    stored: S,
}

impl<S: Stored> Storer<S> {
    fn create<C: Creator>(creator: C) -> anyhow::Result<Self> {
        let stored = creator.create()?;

        Ok(Self { stored })
    }
}

fn main() -> anyhow::Result<()> {
    let creator = ToCreate {};

    let storer = Storer::create(creator)?;
    Ok(())
}
  • Avoid bounds on structs if you can avoid it
    -struct Storer<S: Stored> {
    +struct Storer<S> {
         stored: S,
     }
    
  • The Self alias represents a single type, it can't represent "this type with different type parameters"; use Storer<_> instead of Self and spell out the type you're actually returning
     impl<S: Stored> Storer<S> {
    -    fn create<C: Creator>(creator: C) -> anyhow::Result<Self> {
    +    fn create<C: Creator>(creator: C) -> anyhow::Result<Storer<C::Storer>> {
             let stored = creator.create()?;
    
    -        Ok(Self { stored })
    +        Ok(Storer { stored })
         }
     }
    
  • Now S in that implementation is unused and will be ambiguous when you call create, so instead of defining the associated function for all S, define it for some dummy type
    -impl<S: Stored> Storer<S> {
    +impl Storer<()> {
         fn create<C: Creator>(mut creator: C) -> anyhow::Result<Storer<C::Storer>> {
    
5 Likes

An alternative is to specify that S and C::Storer must be the same:

impl<S: Stored> Storer<S> {
    fn create<C: Creator<Storer=S>>(creator: C) -> anyhow::Result<Self> {
        let stored = creator.create()?;

        Ok(Self { stored })
    }
}

Otherwise, the compiler has to assume that they may be different.

3 Likes

Oh and regarding this, a type parameter is appropriate if you want a single creator type to be able to create multiple different stored types without being parameterized, themselves. For example, you could have the same FruitCreator that implements both Create<Apple> and Create<Banana>. With an associated type, the implementing type could have a parameter (FruitCreator<F>) to almost do the same thing, but each variant of it is a separate type. It depends on the intended use.

Edit: another way it's sometimes described is that type parameters are "input types" and associated types are "output types". If you think about a trait as a type level function, you could think of the type parameters as the function input and the associated types become the output of the function. Each combination of input maps to some output thats always the same for that input.

That doesn't mean the "input types" need to be literal function input, but it occurs in traits like Add that represent an operation.

2 Likes

@quinedot Can you elaborate for my learning? Thanks so much for your answer!

Non-lifetime bounds on structs have to be repeated everywhere you use the struct. They're considered a validity requirement for the struct, but aren't automatically implied elsewhere.

If you don't need the bounds at the definition site, it results in both less boilerplate (repeating bounds everywhere) and more flexibility (e.g. someone can have a Option<Storer<S>> field or Storer<S> enum variant without requiring S: Stored) to not have the bounds.

In this case it popped up for me when I tried to apply bullet three. One alternative would be to use some specific S: Stored instead of (). Another alternative is @ogeon's suggestion (with or without removing the bounds at the definition site).

(Perhaps the most common reason to need bounds at the definition site is because you need to name an associated type.)

4 Likes

Thanks so much!

@quinedot It would appear that I need to bound the struct <S:Stored> if I want to use trait methods on the "generic" (S) type that is being stored, unless I'm missing something?

Example below - is there a cleaner way to do this?

TL;DR You can still remove the bound on the struct definition, but you don't have to. I don't have any new suggestions.


You have to introduce impl<S:Stored> wherever you need to use Stored capabilities, because that's how generic work in Rust. But if you put that bound on the struct definition, you'll have to introduce the bound everywhere you use Storer<SomeGeneric>, not just those places where you need Stored capabilities. So putting the bound on the struct definition doesn't save you any typing in that respect. (And it's less flexible if there happen to be use cases that don't need the capabilities.)

I deleted the bound on the struct in your playground and nothing changed, i.e., it still compiled -- because the change was just loosening a bound that had to be stated everywhere.

-struct Storer<S:Stored> {
+struct Storer<S> {

If you're happy with what you have, you can leave the bound if you prefer. Not putting bounds on the struct definition is a general guideline / idiomatic approach, but adding having[1] the bounds isn't some grave offense.


  1. edit: adding bounds is a breaking change to downstream; I meant having them from the start ↩ī¸Ž

3 Likes