异步闭包 MVP:征集测试!

2024 年 8 月 9 日 · Michael Goulet 代表 异步工作组

异步工作组很高兴地宣布,RFC 3668“异步闭包”最近已获得 Lang 团队的批准。在这篇文章中,我们想简要说明为什么存在异步闭包,解释它们目前的缺点,最重要的是,宣布在 nightly Rust 上测试它们的号召。

背景故事

异步闭包最初在 RFC 2394 中提出,该 RFC 将 async/await 引入了该语言。自实现 async-await 以来,简单处理异步闭包的功能已经在 nightly 中存在不久之后,但直到最近,异步闭包只是简单地转化为返回异步块的闭包。

let x = async || {};

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

这有一个根本的限制,即不可能表达一个返回借用捕获状态的 future 的闭包。

在某种程度上相关的是,在被调用者方面,当用户想要将异步闭包作为参数时,他们通常将其表达为两个不同泛型类型的约束。

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

这也导致了一个额外的限制,即在不使用 boxing 的情况下,不可能使用这种方式表达高阶异步 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 关于异步闭包和借用的博客文章,以及后来在 compiler-errors 关于 为什么异步闭包是现在这种方式 的博客文章中详细说明。

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

最近的 工作 集中于重新实现异步闭包,使其能够借用,并设计一组异步 fn trait。虽然异步闭包已经以语法形式存在,但这项工作引入了一个新的异步 fn trait 系列,这些 trait 由异步闭包(以及所有其他返回 future 的可调用类型)实现。它们可以这样写

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

(目前,确切如何拼写此边界是一个 未决问题,因此两种语法并行实现。)

RFC 3668 详细阐述了这项实现工作的动机,确认我们需要一流的异步闭包和异步 fn trait,这使我们能够表达异步闭包的借用能力——如果您对整个故事感兴趣,请阅读 RFC!

那么我如何提供帮助?

我们希望您测试这些新功能!首先,在最近更新的 nightly 编译器上,启用 #![feature(async_closure)](请注意,由于历史原因,此功能名称不是复数形式)。

异步闭包的设计目的是(几乎在所有情况下)与返回异步块的闭包向下兼容

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

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

并且在被调用者方面,编写异步 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!() }

或者,如果您正在使用 boxing 模拟高阶异步闭包

// 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!() }

与异步生态系统交互的缺点

如果您打算重写您的异步项目,则需要注意一些缺点。

您不能直接命名输出 future

当您使用样式,在第一类异步 fn trait 边界之前,命名异步可调用边界时,由于需要使用两个类型参数的副作用,您可以在边界的 Future 部分添加额外的边界(例如,+ Send+ 'static),例如

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

目前没有办法对调用异步闭包返回的 future 设置类似的边界,因此如果您需要像这样约束回调 future,那么您还不能使用异步闭包。

我们希望在中/长期通过 返回类型表示法语法 支持此功能。

闭包签名推断的细微差别

将异步闭包传递给泛型 impl Fn(A, B) -> C 边界可能并不总是急切地将闭包的参数推断为 AB,有时会导致奇怪的类型错误。有关此示例,请参阅 rust-lang/rust#127781

我们希望在未来改进异步闭包签名推断。

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

一些库将其回调作为函数指针 (fn()) 而不是泛型。异步闭包目前不实现从闭包到 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!() }