🐙

Rust 1.75.0におけるtrait内のasync fn

2024/01/13に公開

2023年の12月28日にRust 1.75.0がリリースされました

https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html

今回の更新の目玉の一つがasync fnをtrait内で使えるようになったことです。詳しい解説が別のブログポストで出ています。

https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html

今年の頭に安定化への道についてブログが発表されついにここまで来たか、という感じですね。
とはいえ、現段階では様々な制限があるようです。
この記事では現段階でできること・できないことを詳しく見ていこうと思います。

今回できるようになったこと

async fnがtrait内で使えるようになった、という表現しましたが、正確にはtrait内での関数の戻り値として-> iml Traitが使えるようになった、というのが正体です。async fn-> impl Futureを返す関数のシンタックスシュガーであるため結果的にasync fnも使えるようになりました。
return-position `impl Trait`` in trait (RPITIT)と呼ばれいているようです。

trait MyTrait {
    // async fnが使えるようになった
    // fn func1(&self) -> impl Future<Output = bool>; と書いても良い
    async fn func1(&self) -> bool;
    // impl Traitも返せる
    fn func2(&self) -> impl Iterator<Item = &String>;
}

struct MyStruct {
    names: Vec<String>,
}


impl MyTrait for MyStruct {
    async fn func1(&self) -> bool {
        do_async_op().await
    }
    
    fn func2(&self) -> impl Iterator<Item = &String> {
        self.names.iter()
    }
}

できないこと

公式ブログによるとこのasync fnをpublic traitのメソッドで使うことは非推薦としています。
実際に先ほどのコードのMyTraitをpubにすると以下のような警告が出ます

  |
3 |     async fn func1(&self) -> bool;
  |     ^^^^^
  |
  = note: you can suppress this lint if you plan to use the trait only in your own code, or do not care about auto traits like `Send` on the `Future`
  = note: `#[warn(async_fn_in_trait)]` on by default
help: you can alternatively desugar to a normal `fn` that returns `impl Future` and add any desired bounds such as `Send`, but these cannot be relaxed without a breaking API change
  |
3 -     async fn func1(&self) -> bool;
3 +     fn func1(&self) -> impl std::future::Future<Output = bool> + Send;
  |

Send traitは他のスレッドに値を渡すことができることを表すtraitです。このfunc1から返されるFutureがSendを実装していないと他のスレッドに移すことができず、work-stealingと言われるタスクがスレッド間を移動するような環境で動作できません。例えばtokio::task::spawnは引数にFuture + Send + 'staticを要求しています。

このような問題の対処法として、trait-variantというクレートを使うことを提案しています。
このクレートを用いることでSendの制約をつけたtraitとそうでないものを簡単につくることができます。
ドキュメントの例をそのまま引用すると

#[trait_variant::make(IntFactory: Send)]
trait LocalIntFactory {
    async fn make(&self) -> i32;
    fn stream(&self) -> impl Iterator<Item = i32>;
    fn call(&self) -> u32;
}

と書くことで、以下のようなtraitが生成されます。

trait IntFactory: Send {
   fn make(&self) -> impl Future<Output = i32> + Send;
   fn stream(&self) -> impl Iterator<Item = i32> + Send;
   fn call(&self) -> u32;
}

もう一つの制限として、RPITITを使ってしまったtraitはobject safeでなくなり、dynamic dispatchに対応できなくなるとも書かれています。
object safetyについては公式のドキュメントに詳しく書かれていますが、ざっくり言うと実行時にtrait objectをつくることができるかどうかということです。
object safetyを満たしていないtraitはdyn Traitとしては使えなくなります。
今後、trait-variantクレートのアップデートでdynamic dispatchができるようにするユーティリティが提供される予定があるようです。

まとめ

今回の更新でasync fnがtrait内で使えるようになりましたが、注意すべき点がいくつかあります。

  • async fnを公開traitで使う場合、Send制約がつかないのでtrait-variantクレートを用いるかSend制約を明示的につける
  • dynamic dispatchは使えなくなる

現状での制約が煩わしい場合は素直にasync-traitクレートを使い続けるなど従来の方法を使うのがいいかもしれません。
今回の更新は安定化への道のMVP1に相当するものだと思われるので、今後もアップデートが続き使い勝手がさらに良くなりそうです。今後の更新にも期待しましょう!

Discussion