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