jaemk / cached

Rust cache structures and easy function memoization

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Is it completely impossible to cache generic functions?

mhvelplund opened this issue · comments

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.

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

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.

Thanks for the feedback :)