"Hiding" a Generic Type Parameter

I'm building a client for a REST SaaS service we'll call ExampleService, so I have a client struct named ExampleClient.

One component of this is that I've defined a trait for providing credentials to the client:

pub trait CredentialProvider {
    fn provide_token(&self) -> impl Future<Output=Option<&str>>;
}

There are multiple implementations for getting credentials from disk, from an environment variable, from memory, etc. For now, here's an in-memory provider:

pub struct DirectCredentialProvider {
    token: String,
}

impl CredentialProvider for DirectCredentialProvider {
    async fn provide_token(&self) -> Option<&str> {
        Some(&self.token.as_str())
    }
}

Note that it's important for users to have the ability to define their own implementations if they want/need them, e.g. a Hashicorp Vault credential provider.

Finally, here's my client struct:

pub struct ExampleClient<P> {
    credential_provider: P,
}

Unfortunately, as I add more functionality to the client, I fear that the generics are only going to increase as there will likely be similar features for providing different implementations of things, and so I'm trying to figure out how to "erase" the <P> from the type definition.

The most obvious solution is to just box it:

pub struct ExampleClient {
    credential_provider: Box<dyn CredentialProvider>,
}

I feel like I've seen examples of "erasing" the generic parameter in a client struct before, but I admit that I have no idea how it works under the hood.

Are there any other alternatives to just Box<dyn CredentialProvider>? I want my users to be able to accept a &ExampleClient and not have to deal with the generic parameter(s).

Because the types that implement CredentialProvider can be of different sizes, boxing is needed at some level to produce structs with known sizes.

The simple/usual thing is to box Box<dyn CredentialProvider> as you've done.

While it's possible to instead box the struct that contains a variable sized field such as dyn CredentialProvider, by putting that field at the end of the struct, I haven't done that myself and would have to look up the details.

There are no other tricks that I know of.

Few different options come to mind, some less flexible than others:

  1. Provide a default fallback for the P itself:
pub struct ExampleClient<CP: CredentialProvider = DefaultProvider> { ... }
  1. Extract the logic/setup of the ExampleClient into a generic fn(cp: impl X) -> impl Y:
trait Client: Sized {
    // as per "I want my users to be able to accept a &ExampleClient"
    fn accept(&self);
    // any additional logic
    fn sync(&mut self);
    // `Sized` required here
    fn init() -> Self;
}
fn create_client(cp: impl CredentialProvider) -> impl Client {
    // init/execute, no dynamic dispatch overhead
}
  1. Provide an option for both P (compile-time) and the dyn (dynamic dispatch) route:
// with the `DefaultProvider` being a no-op struct
enum Client<P: CredentialProvider = DefaultProvider> {
    // (1) default, unless your user needs an alternative ...
    Dynamic(Box<dyn CredentialProvider>),
    // (2) ... for a bit more performance sensitive of a context, in which
    // the overhead of dynamic dispatch via `dyn Trait` and/or 
    // heap allocation (via `Box`) is quite undesirable
    Generic(P)
}
4 Likes

if, as you have it, ExampleClient needs to own its credential provider, then some variety of owning pointer is probably going to be required, as @jumpnbrownweasel points out.

If ExampleClient values are sufficiently short-lived, and if the credential provider always outlives them, then ExampleClient could also borrow the provider, as an &'c dyn CredentialProvider. You'd still need to make ExampleClient generic in 'c, but you may be able to keep the number of parameterized lifetimes under control by using the same lifetime for multiple borrowed fields; so long as there is some lifetime they all satisfy, this works fine.

Which is appropriate is really a design question.

The other significant alternative, if the set of types implementing CredentialProvider is knowable in advance, is to provide an enum for them that also implements CredentialProvider:

#[non_exhaustive] // discourage callers from `match`ing on it
pub enum AnyCredentialProvider {
  Direct(DirectCredentialProvider),
}

impl CredentialProvider for AnyCredentialProvider {
  async fn provide_token(&self) -> Option<&str> {
    match self {
      Self::Direct(direct) => direct.provide_token().await,
      // ...
    }
  }
}

There are crates, like enum_dispatch, that generate this kind of adapter for you, if you don't want to maintain the boilerplate by hand.

4 Likes

Thanks for your response!

For your first example:

pub struct ExampleClient<CP: CredentialProvider = DefaultProvider> { ... }

When I am passing around a reference to the client, like so:

fn do_with(client: &ExampleClient) {
    // do something
}

In this case do I need to specify the parameters in &ExampleClient?

Nope, not necessary. Take a look at this snippet, if you want another example.

In your case, you can define a fn do_with_provider<P: CredentialProvider>(c: Client<P>) instead of the fn do_with_custom<T>(), as per the playground. Your fn do_with(c: Client) will remain an fn(Client<DefaultProvider>), in the meantime.

So I made a more expanded version of your example (playground link):

// define credential provider trait and implementations

pub trait CredentialProvider {
    fn provide_token(&self) -> Option<String>;
}

pub struct EnvVarProvider {
    var_name: String,
}

impl CredentialProvider for EnvVarProvider {
    fn provide_token(&self) -> Option<String> {
        std::env::var(&self.var_name).ok()
    }
}

impl Default for EnvVarProvider {
    fn default() -> Self {
        Self {
            var_name: "TOKEN".into(),
        }
    }
}

#[derive(Default)]
pub struct DefaultProvider {
    var: EnvVarProvider
}

impl CredentialProvider for DefaultProvider {
    fn provide_token(&self) -> Option<String> {
        self.var.provide_token()
    }
}

// define client

#[derive(Clone, Default)]
pub struct Client<T: CredentialProvider + Default = DefaultProvider> {
    provider: T,
}

impl<T> Client<T> where T: CredentialProvider + Default {
    pub fn do_request(&self) -> String {
        let _token = self.provider.provide_token().unwrap();
        "success".into()
    }
}

pub fn call_with_client_erased(_client: &Client) {
    eprintln!("erased");
}

pub fn call_with_client_specific<T: CredentialProvider + Default>(_client: &Client<T>) {
    eprintln!("explicit");
}

// define client owner
pub struct Owner {
    client: Client,
}

impl Owner {
    pub fn client(&self) -> &Client {
        &self.client
    }
}

#[test]
fn test_direct() {
   let client = Client::default();
   
   call_with_client_erased(&client);
   call_with_client_specific(&client);
}

#[test]
fn test_owner() {
    let owner = Owner {
        client: Client::default(),
    };
    
    call_with_client_erased(&owner.client);
    call_with_client_specific(&owner.client);
}

#[test]
fn test_no_call() {
    let _client = Client::default();
}

As long as a Client is used in some way, everything works well, but the last test:

#[test]
fn test_no_call() {
    let _client = Client::default();
}

This fails to compile:

error[E0283]: type annotations needed for `Client<_>`
  --> src/lib.rs:89:9
   |
89 |     let _client = Client::default();
   |         ^^^^^^^   ------ type must be known at this point
   |
   = note: cannot satisfy `_: CredentialProvider`
   = help: the following types implement trait `CredentialProvider`:
             DefaultProvider
             EnvVarProvider
note: required by a bound in `Client`
  --> src/lib.rs:39:22
   |
39 | pub struct Client<T: CredentialProvider + Default = DefaultProvider> {
   |                      ^^^^^^^^^^^^^^^^^^ required by this bound in `Client`
help: consider giving `_client` an explicit type, where the type for type parameter `T` is specified
   |
89 |     let _client: Client<T> = Client::default();
   |                +++++++++++

For more information about this error, try `rustc --explain E0283`.

I can fix this by simply adding a type to the variable binding:

let client: Client = Client::default();

Is there a workaround for this that doesn't involve typing the variable binding?

You can make that code not have a type ambiguity by making Client implement Default less generically, i.e.

impl Default for Client<DefaultProvider> {...}

But you may not want to actually do that, and it's relying on Rust's fragile “only one impl” type inference behavior. I would say: just use the explicit type.

Another option is

    let _client = <Client>::default();

which uses the syntax for specifying a type instead of a path (you may have seen it in cases like <[_]>::len() for specifying types that don't have path names) and thus makes use of the generic parameter defaults just like let _client: Client does.

That's an interesting case, actually. Your #[derive(Default)] expands to:

impl<T: CredentialProvider + Default> Default for Client<T> {
    fn default() -> Self {
        Self { provider: Default::default() }
    }
}

Yet that expansion itself is generic over <T> - which is where your error is coming from.

Just be explicit about it:

impl Default for Client /* <DefaultProvider> */ { 
    fn default() -> Self {
        Self { provider: Default::default() }
    }
}

On the side note, you may want to drop the Default requirement on the <T> of your Client - especially if the only reason you've added it was to get your Client::default() to work.

The rest of your code will be just fine and ever so slightly more readable, as a bonus.

1 Like

Thank you so much! That's exactly what I needed, and without even boxing!