Is there a way to change this closure to an async one?

I have services with trait like:

#[async_trait::async_trait]
pub trait ServiceOne: Send + Sync {
    async fn player_update(
        &self,
        id: &str,
        lambda: &(dyn Fn(Player) -> Result<Player> + Sync),
    ) -> Result<Player>;
}

which I use like this:

pub struct UpdateHandler {
    service_1: Arc<dyn ServiceOne>,
    service_2: Arc<dyn ServiceTwo>,
    service_3: Arc<dyn ServiceThree>,
}

impl UpdateHandler {
    pub fn new(
      service_1: Arc<dyn ServiceOne>,
      service_2: Arc<dyn ServiceTwo>,
      service_3: Arc<dyn ServiceThree>,
    ) -> Self {
      Self { service_1, service_2, service_3 }
    }

    pub async fn handle(&self, input: PlayerInput) -> Result<Player> {
        let output = self.service_1.player_update(&input.id, & |player_from_db| {
            // this closure is a DB transaction; if something fails in here everything is rolled back

            let new_player = Player::update(player_from_db, input)?;

            self.service_2.player_update(&input.id, &new_player).await?;
            
            self.service_3.player_update(&input.id, &new_player).await?;

            Ok(new_player)
        })
        .await?;

        Ok(output)
    }
}

I would like to know if and how to transform that closure (called lambda) in an async one, because as you can see I need to call .await many times in that closure.

If it's not possible is there another way to organize my code? (Among other things I also think that my lambda definition is not totally efficient and idiomatic; by the way, do you have hints?).

I need to call those methods in the closure because that closure is a database transaction: if any service call fails that transaction is rolled back.

If there is NO way to have async closures with STABLE Rust, is there a way with UNSTABLE, nightly Rust using some feature flags?

Just make the closure return a boxed future trait object. Depending on how you're using it you may need to add Send bounds on the trait object too.

use std::{future::Future, pin::Pin, sync::Arc};

use anyhow::Result;

pub struct Player;

impl Player {
    pub fn update(player: Player, input: PlayerInput) -> Result<Player> {
        Ok(player)
    }
}

#[derive(Clone)]
pub struct PlayerInput {
    id: String,
}

#[async_trait::async_trait]
pub trait ServiceOne: Send + Sync {
    async fn player_update(
        &self,
        id: &str,
        lambda: &(dyn Fn(Player) -> Pin<Box<dyn Future<Output = Result<Player>>>> + Sync),
    ) -> Result<Player>;
}

pub struct UpdateHandler {
    service_1: Arc<dyn ServiceOne>,
}

async fn async_fn() {}

impl UpdateHandler {
    pub fn new(service_1: Arc<dyn ServiceOne>) -> Self {
        Self { service_1 }
    }

    pub async fn handle(&self, input: PlayerInput) -> Result<Player> {
        let output = self
            .service_1
            .player_update(&input.id, &|player_from_db| {
                // this closure is a DB transaction; if something fails in here everything is rolled back

                let input = input.clone();
                Box::pin(async {
                    let new_player = Player::update(player_from_db, input)?;

                    async_fn().await;

                    Ok(new_player)
                })
            })
            .await?;

        Ok(output)
    }
}

I created a reproduction here: Rust Playground.

Can you help me understand what's the problem?

You're trying to borrow from the context of the closure, but the future the closure returns needs to be 'static by default. You can add a lifetime bound to the trait object in this case. I also had to add a Send bound so the future would work with tokio.

Here's the new trait definition:

#[async_trait::async_trait]
pub trait ServiceOne: Send + Sync {
    async fn player_update<'a>(
        &'a self,
        id: &str,
        lambda: &(dyn Fn(Player) -> Pin<Box<dyn Future<Output = Result<Player>> + Send + 'a>>
              + Sync),
    ) -> Result<Player>;
}

And here's the full example, including running fake versions of the trait (to make sure the lifetimes are actually usable)
Playground

#![allow(dead_code, unused_variables)]
use anyhow::Result;
use std::{future::Future, pin::Pin, sync::Arc};

#[derive(Clone)]
pub struct PlayerInput {
    id: String,
}

pub struct Player {
    id: String,
    email: String,
}

impl Player {
    pub fn update(player: Player, input: &PlayerInput) -> Result<Player> {
        Ok(player)
    }
}

#[async_trait::async_trait]
pub trait ServiceOne: Send + Sync {
    async fn player_update<'a>(
        &'a self,
        id: &str,
        lambda: &(dyn Fn(Player) -> Pin<Box<dyn Future<Output = Result<Player>> + Send + 'a>>
              + Sync),
    ) -> Result<Player>;
}

#[async_trait::async_trait]
pub trait ServiceTwo: Send + Sync {
    async fn player_update(&self, id: &str) -> Result<Player>;
}

pub struct UpdateHandler {
    service_1: Arc<dyn ServiceOne>,
    service_2: Arc<dyn ServiceTwo>,
}

impl UpdateHandler {
    pub fn new(service_1: Arc<dyn ServiceOne>, service_2: Arc<dyn ServiceTwo>) -> Self {
        Self {
            service_1,
            service_2,
        }
    }

    pub async fn handle(&self, id: &str, input: PlayerInput) -> Result<Player> {
        let output = self
            .service_1
            .player_update(&input.id, &|player_from_db| {
                Box::pin(async {
                    let new_player = Player::update(player_from_db, &input)?;

                    self.service_2.player_update(id).await?;

                    Ok(new_player)
                })
            })
            .await?;

        Ok(output)
    }
}

struct FakeService;

#[async_trait::async_trait]
impl ServiceOne for FakeService {
    async fn player_update<'a>(
        &'a self,
        id: &str,
        lambda: &(dyn Fn(Player) -> Pin<Box<dyn Future<Output = Result<Player>> + Send + 'a>>
              + Sync),
    ) -> Result<Player> {
        println!("ServiceOne");
        let p = Player {
            id: id.to_string(),
            email: "test@example.com".to_string(),
        };

        lambda(p).await
    }
}

#[async_trait::async_trait]
impl ServiceTwo for FakeService {
    async fn player_update(&self, id: &str) -> Result<Player> {
        println!("ServiceTwo");
        Ok(Player {
            id: id.to_string(),
            email: "test@example.com".to_string(),
        })
    }
}

#[tokio::main]
async fn main() {
    UpdateHandler {
        service_1: Arc::new(FakeService),
        service_2: Arc::new(FakeService),
    }
    .handle(
        "1",
        PlayerInput {
            id: "2".to_string(),
        },
    )
    .await
    .unwrap();
}
1 Like

STUNNING BEAUTIFUL CODE! THANK YOU VERY MUCH! :smile:

I accomplished similar solution (thanks to Discord's help), but your is better a lot!

Two doubts only:

  1. on Discord they helped me with a code like:

    Box::pin(async move {
      //...
    }
    

    I saw you used

    Box::pin(async { // move is missing
    

    What's the difference? What should I use in this case?

    Both works but if I use the move it forces me to use some .clone() before the Box::pin(async move {.

  2. Given I need to use this code with a lot of entities (example: Player, Coach, Team, City, Car, Arbiter and so on...): is there a way to "generalize" those lambdas?

async move {
     ...
}

Creates a future by taking ownership of any values outside the async block you reference. In general async move causes less problems because it means the future won't be borrowing from the context.

In your specific case

  1. You immediately await the future from the closure, and immediately await the future that's produced by that method call as well
  2. You have a convenient parameter to limit the lifetime of the returned future to (the reference to self)

So it's easy enough to just use a plain async block, because all of those references are guaranteed to still be around. In more complicated scenarios it's often much less of a headache to clone your data and use async move though

1 Like

Thanks for this explanation.

For those like me who want to learn Rust these words of yours are pure gold.

And you know how to well explain things like few people.

Thanks again and if you have your own blog I would love the RSS address to subscribe.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.