Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

async-trait を使ってみる

完全な小ネタです。使ってみた記事です。

Rust ではトレイトの関数を async にできない

Rust では、現状トレイトのメソッドに async をつけることはできません*1。つまり、下記のようなコードはコンパイルエラーとなります。

trait AsyncTrait {
    async fn f() {
        println!("Couldn't compile");
    }
}
async-trait-sandbox is 📦 v0.1.0 via 🦀 v1.44.0 on ☁️  ap-northeast-1
❯ cargo check
    Checking async-trait-sandbox v0.1.0
error[E0706]: functions in traits cannot be declared `async`
  --> src/main.rs:8:5
   |
8  |       async fn f() {
   |       ^----
   |       |
   |  _____`async` because of this
   | |
9  | |         println!("Couldn't compile");
10 | |     }
   | |_____^
   |
   = note: `async` trait functions are not currently supported
   = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait

async-trait

トレイトのメソッドに async をつけるためには、async-trait というクレートを利用する必要があります。

github.com

試す前の準備

この記事では、次のクレートを用いて実装を行います。

  • async-trait: 今回のメインテーマです。
  • futures: 後ほど async で定義された関数を実行するために使用します。

使用したバージョンは下記です。

[dependencies]
async-trait = "0.1.36"
futures = "0.3.5"

async-trait とは

async 化したいトレイトに対して #[async_trait] というマクロを付け足すことで async fn ... という記法を可能にしてくれるスグレモノです。次のコードはコンパイルが通るようになり、自身のアプリケーションで利用可能になります。

use async_trait::async_trait;

#[async_trait]
trait AsyncTrait {
    async fn f() {
        println!("Could compile");
    }
}

中身はマクロ

どのような仕組みで動いているのでしょうか。#[async_trait] アトリビュートの中身を少し確認してみましょう。

// (...)
extern crate proc_macro;

// (...)

use crate::args::Args;
use crate::expand::expand;
use crate::parse::Item;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;

#[proc_macro_attribute]
pub fn async_trait(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as Args);
    let mut item = parse_macro_input!(input as Item);
    expand(&mut item, args.local);
    TokenStream::from(quote!(#item))
}

Procedural Macros のオンパレード*2のようです。ということで、マクロがどう展開されているのかを見てみましょう。cargo-expand という cargo のプラグインを利用すると、展開後のマクロの状況を知ることができます。

github.com

実際に使ってみると:

❯ cargo expand
    Checking async-trait-sandbox v0.1.0
    Finished check [unoptimized + debuginfo] target(s) in 0.18s

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;
use async_trait::async_trait;
fn main() {}
pub trait AsyncTrait {
    #[must_use]
    fn f<'async_trait>() -> ::core::pin::Pin<
        Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
    > {
        #[allow(
            clippy::missing_docs_in_private_items,
            clippy::needless_lifetimes,
            clippy::ptr_arg,
            clippy::type_repetition_in_bounds,
            clippy::used_underscore_binding
        )]
        async fn __f() {
            {
                ::std::io::_print(::core::fmt::Arguments::new_v1(
                    &["Could compile\n"],
                    &match () {
                        () => [],
                    },
                ));
            };
        }
        Box::pin(__f())
    }
}

async fn f() というメソッドは、マクロによって fn f<'async_trait>() -> ::core::pin::Pin<Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>> へと展開されています。では肝心の async はどこに行ってしまったのかというと、関数の中にて async ブロックとして処理されています。そもそも async 自体が Future のシンタックスシュガーなので、こういった結果になっているわけです*3

呼び出し

実際に関数を呼び出しをしてみましょう。次のようなコードを書くと、呼び出しのチェックをできます。

use async_trait::async_trait;
use futures::executor;

fn main() {
    let runner = Runner {};
    executor::block_on(runner.f());
}

#[async_trait]
pub trait AsyncTrait {
    async fn f(&self);
}

struct Runner {}

#[async_trait]
impl AsyncTrait for Runner {
    async fn f(&self) {
        println!("Hello, async-trait");
    }
}

これでコンパイルを通せるようになります。実行してみると、

❯ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/async-trait-sandbox`
Hello, async-trait

しっかり意図したとおりの標準出力を出してくれました。便利ですね。

*1:一応 RFC は出ています→https://github.com/rust-lang/rfcs/issues/2739

*2:この記事の主題ではなくなってしまうので Procedural Macros に関する解説はしませんが、この記事で使い方をかなり詳しく解説してくれています。

*3:かなり説明を端折ってしまっています。こちらのドキュメントに詳細が書いてあります。