skip to Main Content

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


  1. 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 a Box, 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 the async-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 use async-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:

    struct SaleorApp {
      pub apl: Box<dyn APL>
    }
    
    #[async_trait]
    pub trait APL: Send + Sync + std::fmt::Debug {
        async fn get(&self, saleor_api_url: &str) -> Result<AuthData>;
        async fn set(&self, auth_data: AuthData) ->  Result<()>;
        async fn delete(&self, saleor_api_url: &str) -> Result<()>;
    }
    
    pub fn create_app(config: Config) -> anyhow::Result<SaleorApp> {
        use AplType::{Env, File, Redis};
        Ok(SaleorApp {
            apl: match config.apl {
                Env => Box::new(EnvApl {}),
                File => Box::new(FileApl { path: "apls.txt" }),
                Redis => Box::new(RedisApl{}),
            },
        })
    }
    

    Note that Clone and Sized cannot be implemented for trait objects because they require Sized, and trait objects don’t have a size known at compile time. Thus, I’ve omitted their inclusion above.

    Login or Signup to reply.
  2. Unless you know you’ll always use dynamic dispatch, prefer the former (but drop the constraint on the type if possible):

    struct SaleOrApp<A> {
        pub apl: A
    }
    

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search