Associated type lifetime

Hello,

I have a bunch of different types that can provide values, so I want to abstract using a trait like this:

trait ValueProvider {
  type T,
  fn value(&self) -> Self::T
}

So I can implement value providers like this:

struct IntProvider {
    value: u64
}

impl ValueProvider for IntProvider {
    type T = u64;
    fn value(&self) -> u64 { self.value }
}

What about a struct that owns a String but only wants to provide a &str?

struct StringProvider {
  value: String
}

impl ValueProvider for StringProvider {
  type T = &str;
  fn value(&self) -> &str { self.value }
}

But in type T = &str, a lifetime parameter is missing:

   Compiling playground v0.0.1 (/playground)
error: in the trait associated type is declared without lifetime parameters, so using a borrowed type for them requires that lifetime to come from the implemented type

Where do I get that lifetime parameter from? Or does the entire approach not work?

Thanks!

1 Like

If you desugar the lifetime

  fn value(&self) -> Self::T

to

  fn value<'a>(&'a self) -> Self::T

and notice that your desired use-case

  fn value(&self) -> &str { self.value }

now looks like

  fn value<'a>(&'a self) -> &'a str { self.value }

it should hopefully become clear, that T simply needs to become a GAT (generic associated type), so you can use fn value<'a>(&'a self) -> Self::T<'a>.


trait ValueProvider {
    type T<'a>
    where
        Self: 'a;
    fn value(&self) -> Self::T<'_>;
}

struct IntProvider {
    value: u64,
}

impl ValueProvider for IntProvider {
    type T<'a> = u64;
    fn value(&self) -> u64 {
        self.value
    }
}

struct StringProvider {
    value: String,
}

impl ValueProvider for StringProvider {
    type T<'a> = &'a str;
    fn value(&self) -> &str {
        &self.value
    }
}

(playground)

6 Likes

You can use a generic associated type to pass through the lifetime dependent on the &self lifetime:

trait ValueProvider {
  type T<'a> where Self: 'a;
  fn value(&self) -> Self::T<'_>;
}

struct StringProvider {
  value: String
}

impl ValueProvider for StringProvider {
  type T<'a> = &'a str;
  fn value(&self) -> &str { &self.value }
}

GATs are a relatively new feature and may have limitations and worse error messages than designing your trait in a way that doesn't require them. One such way would be to implement your original trait for a reference:

impl<'a> ValueProvider for &'a StringProvider {
  type T = &'a str;
  fn value(&self) -> &'a str { &self.value }
}

But the price that you pay then is that the bound StringProvider: ValueProvider doesn't hold; every bound has to mention the reference lifetime.

5 Likes

Awesome, thanks!

Thanks!

Actually, I realize that GATs currently come with some limitations that make them unsuitable for my current use case. In particular, I'd like to create a StringProvider and then use it as a dyn ValueProvider, which is not currently (or ever?) possible. But thanks for the detailed answer!

When trying to create a trait object for ValueProvider:

        let x = IntProvider { value: 123 };
        let y: &dyn ValueProvider = &x;

The compiler gives two "help" suggestions at the bottom of the error message:

error[E0038]: the trait `test::basic_gat::ValueProvider` cannot be made into an object
  --> src/lib.rs:53:16
   |
53 |         let y: &dyn ValueProvider = &x;
   |                ^^^^^^^^^^^^^^^^^^ `test::basic_gat::ValueProvider` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> src/lib.rs:28:18
   |
27 |         trait ValueProvider {
   |               ------------- this trait cannot be made into an object...
28 |             type T<'a>
   |                  ^ ...because it contains the generic associated type `T`
   = help: consider moving `T` to another trait
   = help: the following types implement the trait, consider defining an enum where each variant holds one of these types, implementing `test::basic_gat::ValueProvider` for this new enum and using it instead:
             test::basic_gat::IntProvider
             test::basic_gat::StringProvider

But I don't see how T can be moved to another trait, since it is needed in ValueProvider. And implementing ValueProvider for an enum wouldn't allow returning different types for each variant.

Anyone know: Are these suggestions invalid, or is there something I'm missing about how to apply them?

Actually, I realize you cannot even have associated types if you want to have object safety, so I have to take a different approach altogether.

Trait objects with associated types work, but the associated types can't have generic type params, e.g., lifetimes.

trait ValueProvider {
    type T;
    fn value(&self) -> Self::T;
}
struct IntProvider {
    value: u64,
}
impl ValueProvider for IntProvider {
    type T = u64;
    fn value(&self) -> u64 {
        self.value
    }
}
struct StringProvider {
    value: String,
}
impl ValueProvider for StringProvider {
    type T = usize;
    fn value(&self) -> usize {
        self.value.parse().unwrap()
    }
}
let x = StringProvider { value: "123".into() };
let y: &dyn ValueProvider<T = usize> = &x;
assert_eq!(123, y.value());

Understood. I was hoping the suggestions in the error message would point toward alternatives, but they seem to be invalid.

For any solution that works with trait objects, you will always have to specify the associated type when creating the trait object as I did above. That makes sense, since the trait fn must return a known type. But is that really going to work for you?

The same thing would be true if you made the trait generic ValueProvider<T> rather than use an associated type.

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.