我还在学习 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 以使其编译?