how can I create a sort of code, where I can just call
let saleor_app = SaleorApp::new(config);
let saleor_app.apl.get("10.1:3000/gql/")
where SaleorApp has a dyn Trait / Generic field, that allows me to call a function and have any one of different APLs/handlers doing the underlying work? It would get chosen via env variable. I know I need traits for this, but I’m unsure if SaleorApp should be
struct SaleorApp<A: APL> {
pub apl: A
}
Or
struct SaleorApp {
pub apl: Box<dyn APL>
}
The trait:
pub trait APL: Send + Sized + Sync + Clone + std::fmt::Debug {
fn get(&self, saleor_api_url: &str) -> impl Future<Output = Result<AuthData>> + Send;
fn set(&self, auth_data: AuthData) -> impl Future<Output = Result<()>> + Send;
fn delete(&self, saleor_api_url: &str) -> impl Future<Output = Result<()>> + Send;
}
Either way I can’t get it to work, because of multiple reasons.
As for having a Box, I fall short because the trait APL
cannot be made into an object, as it returns impl Futures
, and async traits are maybe maybe not allowed(Apparently they got stabilized in 1.75, but they don’t work for me). Boxing the whole return type of all functions in the trait is the only solution I found, but having all that happen on the heap doesn’t sound very Rusty.
And when trying to make it with generics, I fall flat because of:
pub fn create_app<A: APL>(config: Config) -> anyhow::Result<SaleorApp<A>> {
use AplType::{Env, File, Redis};
SaleorApp {
apl: match config.apl {
Env => EnvApl {}?,
File => FileApl { path: "apls.txt" }?,
Redis => RedisApl::new(config.apl_url, config.app_api_base_url)?,
},
}
}
Gives error:
`match` arms have incompatible types
expected `RedisApl`, found `EnvApl` [E0308]
or
expected A, found RedisApl
How can I achieve this functionality?
Thanks for tips 🙂
2
Answers
If you want to use generics to decide which implementation to use, then your decision on which to use has to be done at compile time, because Rust is going to emit code specific to the implementation you’ve chosen. Since you want to make this work based on the environment or a configuration option, you need to use a trait object (that is,
dyn
with aBox
,Arc
, reference, or similar).Now, it is true that you can’t use
impl
in a trait object because the compiler needs to know the type (and the size) of the object it’s returning. If you use theasync-trait
crate, it effectively implements this by boxing the return values, as you’d mentioned doing, just with some nicer syntax. You certainly can use the new async trait functionality in 1.75, but because I try to target older versions of Rust as well, I’ve just opted to useasync-trait
.It is true that many times it is nicer and more performant to make these choices statically by using generics instead of trait objects and it can be a little faster to avoid boxing code. However, in your case, there’s not really much of an option if you want to adopt the approach you have, and choosing an implementation at runtime based on configuration is a valid and legitimate choice, so I don’t see a huge problem with that approach.
The code would look a little like this:
Note that
Clone
andSized
cannot be implemented for trait objects because they requireSized
, and trait objects don’t have a size known at compile time. Thus, I’ve omitted their inclusion above.Unless you know you’ll always use dynamic dispatch, prefer the former (but drop the constraint on the type if possible):
If you still need dynamic dispatch you can always use
SaleOrApp<Box<dyn APL>>
as your type and have essentially the same thing as your second definiton, but if you don’t actually need dynamic dispatch, you don’t need to use it and don’t incur the overhead that comes with it.Your function however does need to return
SaleOrApp<Box<dyn APL>>
because it tries to return multiple differnt concrete types, that can’t be done with a generic.