jaemk / cached

Rust cache structures and easy function memoization
MIT License
1.58k stars 95 forks source link

Is it completely impossible to cache generic functions? #139

Closed mhvelplund closed 1 year ago

mhvelplund commented 1 year ago

I have a method that looks roughly like this:

#[cached(time = 30, key = "String", convert = r#"{ user_id.clone() }"#)]
async fn read_user<D>(client: Arc<D>, user_id: String) -> Result<User, DataStoragePlatformError>
where
    D: DynamoDbClient + Send + Sync,
{
    client.read_user(&user_id).await
}

When I compile i get the following error that doesn't make sens to me:

error[E0401]: can't use generic parameters from outer function
  --> project/src/auth/aws.rs:20:35
   |
20 | async fn read_user<D>(client: Arc<D>, user_id: String) -> Result<User, DataStoragePlatformError>
   |          ------------             ^ use of generic parameter from outer function
   |          |         |
   |          |         type parameter from outer function
   |          help: try using a local generic parameter instead: `read_user<D, D>`

I'm guessing that the macro generates some code that is somehow syntactically incorrect, but I'm uncertain if there is a way to rewrite this so it would work? Stripping generics would be hard since it's supposed to work with both a mock and a real implementation.

jaemk commented 1 year ago

The error is coming from the fact that the macro needs to define an inner duplicate function so the code can be conditionally executed. In this case, the macro ends up generating something like this

static CACHE = ...;
async fn read_user<D>(client: Arc<D>, user_id: String) -> Result<User, DataStoragePlatformError>
where
    D: DynamoDbClient + Send + Sync,
{
    async fn read_user_inner<D>(client: Arc<D>, user_id: String) -> Result<User, DataStoragePlatformError>
    where
        D: DynamoDbClient + Send + Sync,
    {
        // function logic moved here so it can be conditionally executed
        client.read_user(&user_id).await
    }
    let key = user_id.clone();
    if let Some(cached) = CACHE.get(key) {
        Ok(cached)
    else {
        let new = read_user_inner(...).await;
        // caching the full result. If you don't want this to happen
        // and instead only want to cache `Ok`s, then add `result = true` to macro args
        CACHE.insert(key, new.clone());
        new
    }
}

And that causes the

 |          |         type parameter from outer function
 |          help: try using a local generic parameter instead: `read_user<D, D>`

I'm not sure if it's possible, but the proc macro may be able to take this into account and use a "local generic parameter" in the inner function definition when the signature has generics defined (https://docs.rs/syn/latest/syn/struct.Signature.html).

Until then, the only solution would be to not use a generic. You could try using a dyn trait instead if you need to support multiple implementations of client

WalterSmuts commented 1 year ago

It should be possible to cache generic functions in theory but Rust doesn't yet support generic static items, which is the hold-up here AFAICT. I think Rust might eventually support it natively but for now it's definitely not possible.

In the meantime, I've written a crate that does exactly this with a minor runtime cost. Have a look at generic singleton. It should do exactly what you need.

mhvelplund commented 1 year ago

Thanks for the feedback :)