【2024最新版】traitにasync fnを含める方法
公約[1]通り、2023年内で async fn
in trait が stable Rust (1.75.0) にやってきました。
(本当にめでたい。本当にありがとうございます。)
この記事では async fn
in trait は何が嬉しいのかを書きます。
ついでにモックについての話もします。
これまでの async trait
これまで trait 内で async fn を定義すると、以下のように async-trait crate を使う必要がありました。
#[async_trait::async_trait]
trait Foo {
async fn foo(&self) -> u32;
}
言語側に async fn を trait 内に書く仕組みがないので、Box<dyn Future>
を返すような関数に変換されていました。
これからの async trait
これからは async fn を trait 内で定義できるようになりました。 async trait は、おそらく正式名称ではなく、"async fn
in trait"が正式な用語だと思います。
async な trait というものはなく、trait 定義の中にfn
とasync fn
を混在させることができるようになっただけなので、"async fn
in trait" (もしくは "async fn
in traits")という表現はしっくりきます。
用語のはなしはここまでにして、先ほど async-trait を使っていた例を async fn
in trait に書き換えてみます。
trait Foo {
async fn foo(&self) -> u32;
}
なんというか、こう、クリーンな感じがしますね。
動作的にもクリーンで、 heap を使わず、Future::poll
がstatic dispatch されます。
static dispatch になることでどのくらい早くなるかは自分はよく調べていませんが、どう考えても効率的です。
ただし、落とし穴が2つあります。
1. Send がつかない
Foo::foo
が返す future がSend
であるかどうかは、それがどのように実装されているかに依存します。
以下のコードを見てください。SendFoo
はSend
になるように実装されていますが、UnsendFoo
はSend
になりません。
struct SendFoo;
struct UnsendFoo;
impl Foo for SendFoo {
async fn foo(&self) -> u32 {
let a = std::rc::Rc::new(0u32);
*a
}
}
impl Foo for UnsendFoo {
async fn foo(&self) -> u32 {
let a = std::rc::Rc::new(0u32);
tokio::time::sleep(std::time::Duration::from_secs(0)).await; // 違い
*a
}
}
以下のようなテストを書くと確認できます。
#[test]
fn test_send() {
let foo = SendFoo;
assert_send(foo.foo());
// 以下はコンパイルエラー
// let foo = UnsendFoo;
// assert_send(foo.foo());
fn assert_send<T: Send>(_value: T) {}
}
これが本当に問題になるのはFoo
を generics で使う場合です。例えば以下のようなコードを見てください。
async fn use_foo<T: Foo + Send + Sync>(foo: &mut T) {
assert_send(async {
foo.foo().await;
});
fn assert_send<T: Send>(_value: T) {}
}
これはコンパイルエラーになります。foo
が具体的な型でないため、どう頑張ってもFoo::foo
の返す future がSend
であることを保証できません。
これは大変なことで、assert_send
の部分をtokio::spawn
に書き換えても同様のコンパイルエラーが出ることになりますし、このような関数を axum や actix-web などの handler として使うこともできません。
これには2つの解決方法(実質的には1つ)があります。
まず1つめは、async fn
を脱糖して "return-position impl trait" にして手動でSend
をつけることです。
こうです:
trait Foo {
fn foo(&self) -> impl Future<Output = u32> + Send;
}
2つ目はtrait_variant::make
マクロを使うことです。これは勝手に1つ目と同じことをやってくれます。
#[trait_variant::make(Foo: Send)]
trait UnsendFoo {
async fn foo(&self) -> u32;
}
これはFoo
とUnsendFoo
の両方を定義してくれるので、もとの trait の名前をUnsendFoo
にするところがポイントです。
UnsendFoo
いる?と思うかもしれませんが、例えばライブラリの APIとしてFoo
を公開するときにはUnsendFoo
も一緒に公開することで、ユーザーは好きな方を使うことができます。
例えば single-thread な async runtime を使う場合はRc
でもなんでもSend
でないものを自由に使えるUnsendFoo
が便利になるわけです。
マクロから卒業できると思ったのにまたマクロを使うことになるのか感はありますが、いずれ trait のメソッドの impl Trait に対してトレイト境界をつけることができるようになりそうなので、そのときには本当にマクロを使わなくて済むようになると思います。
2. オブジェクトセーフでない
端的に言うとasync fn
を使ったFoo
のようなトレイトを&dyn Foo
やBox<dyn Foo>
のようにして扱うことができません。
async fn のような返り値にimpl Trait
を使うものは実質 trait の associated type だからですね。
async-trait を使ったときに&dyn Foo
が使えたのは、Box
で型消去しているからです。
なのでdyn Foo
を使いたい場合は、これまで通りasync-trait
を使うか、型消去されたバージョンを手書きしましょう。
後者を自動で実装するようなマクロがリリースされる予定とのことです。
async fn
in trait とモック
async_trait
マクロを消したとき、
mockallはそのまま#[mockall::automock]
をつけたままで動きます。
拙作のmryは対応していなかったのですが、さっき対応したバージョンをリリースしたので#[mry::mry]
をつけたままで動くはずです。
モックライブラリを作る側からすると、async fn
in trait はとてもありがたいです。
async_trait
を使う際はasync_trait
マクロの上にモック用のマクロを書くか下に書くかでどのようなモックコードが生成されるかが変わってしまったり、mry
にいたっては下側に置くことができませんでした。
また、async fn
in trait が標準化されたことで、モックライブラリ側としては特殊な実装をする必要がなくなりました。モックの作り方によるのかもしれませんが、普通の同期関数用のfn
の処理をそのまま使うことができるようになりました。
先ほどのmry
の対応も新しい処理を入れるのではなく、async
をつけ忘れていた部分を修正するだけで済みました。
加筆: impl Future
やtrait_variant::make
を考慮するのが大変ということが判明しました。
mry
は今後は積極的にはメンテナンスしないつもりで、その代わりに、anonymous-traitというライブラリを作っています。
Javaの匿名クラスのように、traitを匿名で実装することができるライブラリです (anonymous trait implementation)。
モックのためのライブラリではないですが、これをモックに使うことができます。
これを使うと、production code である trait 定義に対して#[mry::mry]
や#[mockall::automock]
といったモック向けのマクロをつける必要がなくなるのが、潔癖症の自分にはとてもありがたいです。
let num = 42;
#[anonymous_trait::anonymous_trait(let mock = ())]
impl Foo for () {
async fn foo(&self) -> u32 {
num
}
}
assert_eq!(mock.foo().await, 42);
これが動きます。num
をキャプチャ〜できているのが嬉しいですね。
リリースしたばかりなので荒削りも荒削りですが、使ってフィードバックをいただけるとありがたいです。
まとめ
-
async fn
in trait と return-position impl trait がやってきた - async fn には
Send
がつかない - async fn はオブジェクトセーフでない
- async fn はモックライブラリに優しい
-
anonymous-trait
というライブラリを宣伝しました
Discussion