cockscomblog?

cockscomb on hatena blog

Rust 1.75.0のasync fn in traits

Rustでツールを書こうとして、コンポーネントを差し替えられるようにtraitとして定義した。GUIプログラミングの習い性で、IOが発生するメソッドは非同期にしたいから、asyncキーワードをつける。ここでは、何か文字列を読み込む予定のLoader traitを定義する。

use std::error::Error;

trait Loader {
    async fn load(&self) -> Result<String, Box<dyn Error>>;
}

このコードを書き始めた時点で、このコードは有効ではなかった。Rustではtraitにasyncメソッドを持たせられなかったのだ。そこで、async-trait crateの出番となる。

use std::error::Error;

use async_trait::async_trait;

#[async_trait]
trait Loader {
    async fn load(&self) -> Result<String, Box<dyn Error>>;
}

これを使ってコードを書くと、こういう感じになった。

(Rust Playground)

Rust 1.75.0のasync fn in traits

2023年12月28日に、Rust 1.75.0がリリースされた。

このバージョンから、traitのメソッドをasyncにできるようになった。impl TraitTraitを実装したなんらかの型を表していて、traitのasync fn-> impl Futureの糖衣構文のような扱いになっている。

これを利用すると、async-trait crateを使わなくても同じように書けるはずなので、書き換えてみる。単に#[async_trait]を除去すると、次のようなエラーにぶつかる。

   |
24 |     loader: Box<dyn Loader>,
   |                 ^^^^^^^^^^ `Loader` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>

うわーとなって、「Announcing `async fn` and return-position `impl Trait` in traits | Rust Blog」の記事をよく読み直してみると、制限にぶつかっていることがわかる。特に今回は Dynamic dispatch のところに注目する。

Traits that use -> impl Trait and async fn are not object-safe, which means they lack support for dynamic dispatch. We plan to provide utilities that enable dynamic dispatch in an upcoming version of the trait-variant crate.

動的ディスパッチというのは要するに仮想関数みたいなやつと同じで、コンパイル時に静的に型が決まらないようなケースはまだサポートされていないようだ。ここでさっきのコードを見直すと、確かに、Box<dyn Loader>などとやっている。いずれ公式のtrait-variant crateを使うと、なんらかいい感じにしてくれるようだが、現時点ではまだそういう機能がない。

ということでいったんasync-trait crateに戻ってもよいのだけど、そもそも今回は動的ディスパッチをやめても、いまのところ差し支えない。ChatGPTに「Rustでtraitオブジェクトに対するdynamic dispatchを避けるにはどうしたらよいですか」と聞いてみると、ジェネリクスを使えばいいとわかる(それはそう)。

use std::error::Error;

trait Loader {
    async fn load(&self) -> Result<String, Box<dyn Error>>;
}

struct Processor<L: Loader> {
    loader: L,
}

impl<L: Loader> Processor<L> {
    fn new(loader: L) -> Self {
        return Self { loader };
    }

    async fn process(&self) -> Result<(), Box<dyn Error>> {
        unimplemented!()
    }
}

これで無事にコンパイルできる。

(Rust Playground)

なるほどでした。