Async 闭包 MVP:征集测试!

2024 年 8 月 9 日 · Michael Goulet 代表 Async 工作组发布

Async 工作组兴奋地宣布,Lang 团队最近批准了RFC 3668“Async 闭包”。在这篇文章中,我们将简要说明 async 闭包存在的原因,解释它们当前的缺点,最重要的是,宣布一项征集测试的行动,希望大家在 Nightly Rust 上测试它们。

背景故事

Async 闭包最初在引入 async/awaitRFC 2394中提出。自 async-await 实现后不久,Nightly 版本中就存在了对 async 闭包的简单处理不久之后,但直到最近,async 闭包才简单地解糖为返回 async 块的闭包

let x = async || {};

// ...was just sugar for:
let x = || { async {} };

这有一个根本性的限制,即无法表达一个返回借用捕获状态的 Future 的闭包。

与此 somewhat 相关的是,在被调用者端,当用户希望将 async 闭包作为参数时,他们通常将其表达为两种不同泛型类型的约束

fn async_callback<F, Fut>(callback: F)
where
    F: FnOnce() -> Fut,
    Fut: Future<Output = String>;

这也导致了一个额外的限制,即在不进行装箱的情况下,无法表达使用这种方式的高阶 async fn 约束(因为对 F 的高阶 trait 约束不能导致 Fut 的高阶类型),从而导致不必要的分配

fn async_callback<F>(callback: F)
where
    F: FnOnce(&str) -> Pin<Box<dyn Future<Output = ()> + '_>>;

async fn do_something(name: &str) {}

async_callback(|name| Box::pin(async {
    do_something(name).await;
}));

这些限制在 Niko 关于 async 闭包和借用的博客文章,以及后来 compiler-errors 关于async 闭包为何如此的博客文章中得到了详细阐述。

好的,那么 RFC 3668 有什么帮助?

最近的工作专注于重新实现 async 闭包使其能够进行借用,并设计了一套 async fn trait。尽管 async 闭包作为语法已经存在,但这项工作引入了一个新的 async fn trait 系列,async 闭包(以及所有其他返回 Future 的可调用类型)实现了这些 trait。它们可以写成这样

fn test<F>(callback: F)
where
    // Either:
    async Fn(Arg, Arg) -> Ret,
    // Or:
    AsyncFn(Arg, Arg) -> Ret,

(目前,如何精确地表达这种约束是一个开放问题,因此这两种语法是并行实现的。)

RFC 3668 详细阐述了这项实现工作的原因,确认我们需要一流的 async 闭包和 async fn trait,它们允许我们表达 async 闭包的*借用*能力——如果您对整个故事感兴趣,请阅读该 RFC!

那么我该如何提供帮助?

我们非常希望您测试这些新功能!首先,在一个最近更新的 Nightly 编译器上,启用 #![feature(async_closure)] (请注意,出于历史原因,此特性名称没有复数)。

Async 闭包被设计为可以(几乎在所有情况下)与返回 async 块的闭包进行直接替换

// Instead of writing:
takes_async_callback(|arg| async {
    // Do things here...
});

// Write this:
takes_async_callback(async |arg| {
    // Do things here...
});

在被调用者端,编写 async fn trait 约束,而不是编写返回 Future 的“常规”fn trait 约束

// Instead of writing:
fn doesnt_exactly_take_an_async_closure<F, Fut>(callback: F)
where
    F: FnOnce() -> Fut,
    Fut: Future<Output = String>
{ todo!() }

// Write this:
fn takes_an_async_closure<F: async FnOnce() -> String>(callback: F) { todo!() }
// Or this:
fn takes_an_async_closure<F: AsyncFnOnce() -> String>(callback: F) { todo!() }

或者如果您正在使用装箱模拟高阶 async 闭包

// Instead of writing:
fn higher_ranked<F>(callback: F)
where
    F: Fn(&Arg) -> Pin<Box<dyn Future<Output = ()> + '_>>
{ todo!() }

// Write this:
fn higher_ranked<F: async Fn(&Arg)> { todo!() }
// Or this:
fn higher_ranked<F: AsyncFn(&Arg)> { todo!() }

与 async 生态系统交互的缺点

如果您打算尝试重写您的 async 项目,有几个缺点需要注意。

您不能直接命名输出 Future

当您使用*旧*样式(在有一流 async fn trait 约束之前)命名 async 可调用约束时,由于需要使用两个类型参数,您可以对约束的 Future 部分添加额外的约束(例如 + Send+ 'static'),像这样

fn async_callback<F, Fut>(callback: F)
where
    F: FnOnce() -> Fut,
    Fut: Future<Output = String> + Send + 'static
{ todo!() }

目前没有办法对调用 async 闭包返回的 Future 添加类似的约束,因此如果您需要这样约束您的回调 Future,那么您还不能使用 async 闭包。

我们期望在中长期通过返回类型表示法语法来支持这一点。

闭包签名推断的细微差异

将 async 闭包传递给泛型 impl Fn(A, B) -> C 约束可能不会总是立即将闭包的参数推断为 AB,偶尔会导致奇怪的类型错误。有关此示例,请参见rust-lang/rust#127781

我们期望随着工作的进展改进 async 闭包签名推断。

Async 闭包不能强制转换为 fn() 指针

一些库将其回调接受为函数*指针*(fn())而不是泛型。Async 闭包目前不实现从闭包到 fn() -> ... 的相同强制转换。一些库可能会通过将其 API 调整为接受泛型 impl Fn() 而非 fn() 指针作为参数来缓解此问题。

除非有特别好的理由支持,否则我们不期望实现此强制转换,因为通常可以通过调用者手动使用内部函数项或使用 Fn 约束来处理,例如

fn needs_fn_pointer<T: Future<Output = ()>>(callback: fn() -> T) { todo!() }

fn main() {
    // Instead of writing:
    needs_fn_pointer(async || { todo!() });
    // Since async closures don't currently support coercion to `fn() -> ...`.

    // You can use an inner async fn item:
    async fn callback() { todo!() }
    needs_fn_pointer(callback);
}

// Or if you don't need to take *exactly* a function pointer,
// you can rewrite `needs_fn_pointer` like:
fn needs_fn_pointer(callback: impl async Fn()) { todo!() }
// Or with `AsyncFn`:
fn needs_fn_pointer(callback: impl AsyncFn()) { todo!() }