0

我还在学习 Rust,我正在做的一个练习是移植一个我在其他语言中使用过的“流利的构建器”~esque 模式。例如:

struct CalculationResult {
  input: i32
  output: i32
  for_user: Uuid
}
impl CalculationResult {
  pub fn log_to_file(self, file: &mut File) -> Result<CalculationResult, anyhow::Error> {
    // ...
    Ok(self)
  }
  pub fn into_response(self) -> Response<HttpResponseMessage> {
    Response::new(self.into())
  }
}

允许我编写一个端点处理程序,例如:

fn my_expensive_calculation(input: i32) -> CalculationResult {
  todo!()
}
struct EndpointHandler {
  log_file: Arc<Mutex<File>>
};
impl EndpointHandler {
  pub fn handle(&self, input: i32) -> Result<Response<HttpMessage>, anyhow::Error> {
    Ok(
      my_expensive_calculation(input)
        .log_to_file(self.file.lock().unwrap().deref_mut())?
        .into_response()
    )
  }
}

不幸的是,上面的类型签名CalculationResult::log_to_file是一个谎言。它实际上不需要消耗self。原来它只需要&self为了能够登录到文件。但是,为了保持流畅的风格,我们仍然需要 return self。该怎么办?

重构 1:拯救的特征

更新log_to_file以拥有其真正的签名

pub fn log_to_file(&self, file: &mut File) -> Result<(), anyhow::Error> {
  // ...
  Ok(())
}

并添加一点 trait 魔法来提供“副作用”辅助方法

trait SideEffect: Sized {
  fn side_effect<E, F: FnOnce(&Self) -> Result<(), E>>(
    self,
    effect: F,
  ) -> Result<Self, E> {
    effect(&self)?;
    Ok(self)
  } 
}
impl<A: Sized> SideEffect for A {}

现在处理程序可以写成:

Ok(
  my_expensive_calculation()
    .side_effect(|this| this.log_to_file(self.file.lock().unwrap().deref_mut())?
    .into_response()
)

很容易,但下一个挑战随之而来。假设我们想要将信息保存到数据库,而不是记录到本地文件。

重构 2:进入异步

CalculationResult现在可以将自己保存到远程数据库中,而不是记录到文件中:

impl CalculationResult {
  pub async fn persist(self, pool: &PgPool) -> Result<(), anyhow::Error> {
    sqlx::query("INSERT INTO some_table ...")
     .bind(&self.input)
     .bind(&self.output)
     .bind(&self.for_user)
     .execute(pool)
     .await?;
    Ok(())
  }
  pub fn into_response(self) -> Response<HttpMessage> {
    Response::new(self.into())
  }
}

我想更新处理程序以适应这种异步副作用,同时保持流畅界面的精神完好无损:

struct EndpointHandler {
  db_pool: sqlx::PgPool
};
impl EndpointHandler {
  pub async fn handle(&self, input: i32) -> Result<Response<HttpMessage>, anyhow::Error> {
    Ok(
      task::spawn_blocking(move || my_expensive_calculation(input))
        .await?
        .async_side_effect(|this| this.persist(&self.db_pool))
        .await?
        .into_response()
    )
  }
}

不幸的是,这就是我在 Rust 方面的技能达不到要求的地方。直觉上,我认为这是可能的,因为最终会CalculationResult比未来更长久persist

问题:你如何实现一个AsyncSideEffect特征?

您如何更新SideEffect以便它可以采用产生的函数Future<Output=Result<(), E>>?如果async特性中允许使用函数,它可能是这样的:

trait AsyncSideEffect {
  async fn async_side_effect<Err, Fut, F>(self, effect: F) -> Result<Self, Err> 
  where 
    Fut: Future<Output=Result<(), Err>>,
    F: FnOnce(&Self) -> Fut
  {
    effect(&self).await?;
    Ok(self)
  }
}

但是您今天在 Rust Stable 2021 中如何做到这一点?

编辑:缩小范围(无特征助手)

与其在特征中处理异步功能的开销,让我们通过引入一个结构来将其简化为一个更简单的情况,我们可以在其中添加一个异步函数到它的impl

struct Holder<T> {
    data: T,
}
impl<T> Holder<T> {
    pub async fn async_side_effect<E, Fut: Future<Output = Result<(), E>>, Fn: FnOnce(&T) -> Fut>(
        self,
        map: Fn,
    ) -> Result<T, E> {
        let result = map(&self.data).await;
        match result {
            Ok(_) => Ok(self.data),
            Err(e) => Err(e),
        }
    }
}

更新处理程序以显式构建此帮助器对象,如下所示:

impl EndpointHandler {
  pub async fn handle(&self, input: i32) -> Result<Response<HttpMessage>, anyhow::Error> {
    // spawn_blocking elided for clarity
    let calc_result = my_expensive_calculation(input);
    let holder = Holder { data: calc_result};
    
    Ok(
      holder
        .async_side_effect(|this| this.persist(&self.db_pool))
        .await?
        .into_response()
    )
  }
}

导致编译器给出一个生命周期错误:

error: lifetime may not live long enough
   --> src/calculation/mod.rs:123:33
    |
123 |       .async_side_effect(|this| this.persist(&self.db_pool))
    |                           ----- ^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
    |                           |   |
    |                           |   return type of closure `impl futures::Future` contains a lifetime `'2`
    |                           has type `&'1 calculation::CalculationResult`

是否可以Holder使用生命周期注释或额外的块来更新 struct 或 impl 以使其编译?

4

0 回答 0