'Factory method with associated type
I'm trying to implement a factory method that returns a Service with an associated type. I got it to work without the associated type, but once I add that, I can't get it to compile regardless of how I massage it..
This is the Service:
trait QType {}
trait Service {
type Query: QType;
fn sanitize(&self, query: &str) -> Result<Self::Query, String>;
fn run(&self, query: &Self::Query) -> Result<(), String>;
}
So the idea is that the sanitize function returns an instance of the Query, which can then be passed to the run function.
The factory looks like this (doesn't compile):
fn factory<Q: QType>(name: &str) -> Box<dyn Service<Query = Q>> {
match name {
"amazon" => Box::new(amzn::Amazon {}),
other => panic!("Invalid service {}", other),
}
}
Now I only have one service here and I could be specific in the type Parameters in the signature -- which would make it compile -- but I want to have a generic factory method and add more services.
Here's the implementation of the Amazon service:
mod amzn {
use super::*;
pub struct Amazon {}
pub struct Product {
name: String,
}
impl QType for Product {}
impl Service for Amazon {
type Query = Product;
fn sanitize(&self, query: &str) -> Result<Product, String> {}
fn run(&self, query: &Product) -> Result<(), String> {}
}
}
The compiler says:
error[E0271]: type mismatch resolving `::Query == Q`
--> src/main.rs:9:21
|
9 | "amazon" => Box::new(amzn::Amazon {}),
| ^^^^^^^^^^^^^^^^^^^^^^^^^ expected type parameter, found struct `amzn::Product`
|
= note: expected type `Q`
found type `amzn::Product`
= help: type parameters must be constrained to match other types
= note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters
= note: required for the cast to the object type `dyn Service`
Based on this error message, I 'm not sure how to specify the type parameter. I have tried extracting the creation of Amazon and giving it explicit type parameters, but that just leaves to different errors. Also, following the linked chapter 10.02 in the book doesn't give any explanations on the case with associated types. Lastly, I also tried the route of RFC-1598: Generic Associated Types, but I could neither get it to compile nor am I sure whether I really need that.
Also please note that I added the Box wrapper and QType restriction based on other answers here on SO around similar issues, but I could very be completely on the wrong path here..
Any help is much appreciated.
Solution 1:[1]
This signature is not possible to implement:
fn factory<Q: QType>(name: &str) -> Box<dyn Service<Query = Q>>
An associated type is always uniquely determined by the implementing type. I.e. every implementation of Service chooses just one associated type Query.
This is at odds with factory, which is letting the caller decide what the associated type should be. It should be clear to see that if you call factory with Q that is not Product then the code inside the match expression no longer type checks.
You can make this work by fixing the choice of Query:
fn factory(name: &str) -> Box<dyn Service<Query = Product>> {
match name {
"amazon" => Box::new(amzn::Amazon {}),
other => panic!("Invalid service {}", other),
}
}
If you want the caller to choose the type, then you need to find a way so that the body of the function will work for any choice of Q. For example, you can associate the construction with the QType trait:
trait QType {
fn create_service(name: &str) -> Option<Box<dyn Service<Query = Self>>>;
}
fn factory<Q: QType>(name: &str) -> Box<dyn Service<Query = Q>> {
Q::create_service(name).expect("Invalid service")
}
And implement for your type:
impl QType for Product {
fn create_service(name: &str) -> Option<Box<dyn Service<Query = Self>>> {
match name {
"amazon" => Some(Box::new(amzn::Amazon {})),
other => None,
}
}
}
Solution 2:[2]
I think factory method can be implemented using dynamic dispatch but without associated types.
I have implemented the pattern using your example in this rust playground.
The trait for Service with dynamic dispatch now is looking like the following:
trait Service {
fn sanitize(&self, query: &str) -> Result<Box<dyn QType>, String>;
fn run(&self, query: &dyn QType) -> Result<(), String>;
}
Notice the use of trait objects for the method parameters instead of the concrete types.
Since we actually need to recover the concrete types we will use the Any trait and the AsAny strategy to be able to downcast from trait objects to the concrete types.
use std::any::Any;
trait QType: Any + AsAny {}
trait AsAny {
fn as_any(&self) -> &dyn Any;
}
impl<Q: QType> AsAny for Q {
fn as_any(&self) -> &dyn Any {
self
}
}
fn downcast_ref_qtype<Q: QType> (qtype: &dyn QType) -> Result<&Q, String> {
qtype.as_any().downcast_ref::<Q>().ok_or_else(|| "QType not supported".to_owned())
}
And inside the concrete implementation we can use this downcast_ref_qtype method to get back our concrete type:
impl Service for Amazon {
fn sanitize(&self, query: &str) -> Result<Box<dyn QType>, String> {
// sanitize implementation
// ...
let product = Product {
name: query.to_owned()
};
Ok(Box::new(product))
}
fn run(&self, query: &dyn QType) -> Result<(), String> {
let product: &Product = downcast_ref_qtype(query)?;
// run implementation
// ...
Ok(())
}
}
Since Service trait does not have an associated type any longer now we can implement our factory method:
fn factory(name: &str) -> Box<dyn Service> {
match name {
"amazon" => Box::new(amzn::Amazon {}),
other => panic!("Invalid service {}", other),
}
}
fn main() {
let service = factory("amazon");
service.run(service.sanitize("usb charger").unwrap().as_ref()).unwrap();
}
As a summary we have hidden the associated type inside the concrete implementation of the run method and we define it when calling the downcast function.
Edit: If you still want your associated type I found a way to add another layer which in turn will let you still use your trait with static dispatch if needed.
We add another trait that has the associated type.
trait ServiceImpl {
type Q: QType;
fn sanitize(&self, query: &str) -> Result<Self::Q, String>;
fn run(&self, query: &Self::Q) -> Result<(), String>;
}
And we add a blanket implementation that implements the Service trait for any ServiceImpl implementor and takes care of downcasting.
impl <SI: ServiceImpl> Service for SI {
fn sanitize(&self, query: &str) -> Result<Box<dyn QType>, String> {
Ok(Box::new(ServiceImpl::sanitize(self, query)?))
}
fn run(&self, query: &dyn QType) -> Result<(), String> {
let query = downcast_ref_qtype::<SI::Q>(query)?;
ServiceImpl::run(self, query)
}
}
Now we only need to implement ServiceImpl and the struct can be used both as a trait object and with static dispatch
impl ServiceImpl for Amazon {
type Q = Product;
fn sanitize(&self, query: &str) -> Result<Product, String> {
// sanitize implementation
// ...
let product = Product {
name: query.to_owned()
};
Ok(product)
}
fn run(&self, query: &Product) -> Result<(), String> {
// run implementation
// ...
Ok(())
}
}
You can see the full example here: rust playground
Let me know if this is helpful :)
Best regards, CHBS
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 |
