asynchronousrusthashmap

async function as hashmap value type


I want to store some aysnc operations in a hashmap. Then get a async operation list based on the user input string list. By search the site,I found this question

How can I put an async function into a map in Rust?

The difference is that I want to use the stored operation hashmap across diffrent tokio thread. So I rewrite with once_cell + RwLock.

Full example

static MAPS: Lazy<Arc<RwLock<HashMap<&'static str, CalcFn>>>> = Lazy::new(|| {
    let map = Arc::new(RwLock::new(HashMap::new()));
    let mut map_lock = map.write().unwrap();
    map_lock.insert(
        "add",
        Box::new(
            move |a: i32, b: i32| -> Pin<Box<dyn Future<Output = BoxedResult<i32>>>> {
                Box::pin(add(a, b))
            },
        ) as CalcFn,
    );
    map_lock.insert(
        "sub",
        Box::new(
            move |a: i32, b: i32| -> Pin<Box<dyn Future<Output = BoxedResult<i32>>>> {
                Box::pin(sub(a, b))
            },
        ) as CalcFn,
    );
    map.clone()
});

The lazy initialization seems boring and takes a lot of lines in code beacause I may have 20+ async operations.

Could someone kindly offer some comments to write this example in a "cleaner" way?


Solution

  • The first thing to do is move as much as possible from the insert operation into another function.

    fn insert_async_fn<K, F, Fut>(map: &mut HashMap<K, CalcFn>, name: K, f: F)
    where
        K: Eq + Hash,
        F: Fn(i32, i32) -> Fut + Send + Sync + 'static,
        Fut: Future<Output = BoxedResult<i32>> + Send + 'static,
    {
        map.insert(name, Box::new(move |a, b| Box::pin(f(a, b))));
    }
    
    // Used like this
    insert_async_fn(&mut map, "add", add);
    insert_async_fn(&mut map, "sub", sub);
    

    You don't need to use clone at the end. All you need to do is drop the lock before returning.

    drop(map_lock);
    map
    

    You don't need the Arc. Arc can be used to ensure a value stays alive across multiple threads, but static already ensures that.

    In the code you posted, you also don't need RwLock, but it might be necessary if you are modifying the map after construction.

    If you're using the latest stable version of Rust, you can use LazyLock from the standard library. It works the same as the one from once_cell.

    You usually don't need errors to be Sync, but you do often need futures to be Send, so that they can be spawned on multithreaded runtimes.

    That leaves us with this:

    static MAPS: LazyLock<RwLock<HashMap<&'static str, CalcFn>>> = LazyLock::new(|| {
        let mut map = HashMap::new();
    
        insert_async_fn(&mut map, "add", add);
        insert_async_fn(&mut map, "sub", sub);
    
        RwLock::new(map)
    });
    
    fn insert_async_fn<K, F, Fut>(map: &mut HashMap<K, CalcFn>, name: K, f: F)
    where
        K: Eq + Hash,
        F: Fn(i32, i32) -> Fut + Send + Sync + 'static,
        Fut: Future<Output = BoxedResult<i32>> + Send + 'static,
    {
        map.insert(name, Box::new(move |a, b| Box::pin(f(a, b))));
    }
    

    Rust playground