🕌

Rust のトレイトを使おう!(2)

2021/05/01に公開

お待たせしました,前回の記事の後半になります.

前回は基本的なことでしたが,今回は具体的なクレートを挙げながら,より発展的な使い方を紹介していきます.

Miscellaneous

ケース6:外部の型(foreign type)に impl を追加する

itertools が実際にやっていることですが,外部の型に impl を追加するのにもトレイトは便利です.

外部の型とは,そのクレートの外で定義された(つまり,dependencies の中で定義された)型のことを言います.
Rust では外部の型に新しく impl を追加することはできません.たとえば Vec<Option<u64>> に対して次のようなことはできません:

impl Vec<Option<u64>> {
    fn get_flatten(&self, n: usize) -> Option<&u64> {
        self.get(n).map(Option::as_ref).flatten()
    }
}

そもそも外部の型へ impl を追加するというのは,まずそのプログラムの設計に問題があると疑うべきですが,それでもどうしても追加したいことはあります.その場合はトレイトによって

trait GetFlatten {
    type Item;

    fn get_flatten(&self, n: usize) -> Option<&Self::Item>;
}

impl GetFlatten for Vec<Option<u64>> {
    type Item = u64;

    fn get_flatten(&self, n: usize) -> Option<&Self::Item> {
        self.get(n).map(Option::as_ref).flatten()
    }
}

fn call_get_flatten() {
    let v: Vec<Option<u64>> = Vec::new();
    let item = v.get_flatten(0);
    assert_eq!(item, None);
}

みたいに実現できます.ただし,get_flatten() メソッドを呼ぶためには,GetFlatten トレイトが同じ名前空間に入っている(つまり use GetFlatten されている)必要はあります.

ケース7:ユーザに選択肢を残す

ここで言うユーザは,

  • ライブラリ開発者にとっては使用者,
  • バイナリ開発者にとっては自分自身

を表します.

「ユーザに選択肢を残す」とはどういうことかを説明するために,actix web というクレートを例に取りましょう.Actix web は web サーバを実装するためのクレートです.

Hello World! するだけの web サーバを作りたいとき,ユーザはたとえば次の関数を用意します:

use actix_web::HttpResponse;

fn hello() -> HttpResponse {
    HttpResponse::Ok().body("Hello, World!")
}

そして,actix_web::get マクロや actix_web::Route::to() 関数などを介して,サービスと呼ばれるものを作成します.

use actix_web::{get, App, HttpResponse, HttpServer};

#[get("/")]
async fn hello() -> HttpResponse {
    HttpResponse::Ok().body("Hello, World!")
}


#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(hello))
        .bind("127.0.0.1:8080")?
        .run()
        .await
}

今問題にするのは,hello() 関数の引数です.公式の Getting Started を見ると分かりますが,この引数にはいろんなバージョンがあります:

async fn hello() -> impl Responder {}

async fn echo(req_body: String) -> impl Responder {}

async fn index(data: web::Data<AppState>) -> String {}

async fn index(path: web::Path<(String, String)>, json: web::Json<MyInfo>) -> impl Responder {}

「引数の数が違う」という部分は get マクロや post などが頑張っているわけですが,「引数の型が違う」ことについて注目してみましょう.

上に現れた引数の型 Stringweb::* などは,実はすべて FromRequest トレイトを実装しています
また,actix multipart クレートのように独自で FromRequest を実装し,それを関数の引数とすることもできます.

use actix_multipart::Multipart;
use actix_web::{post, HttpResponse};

#[post("/")]
async fn upload_file(payload: Multipart) -> HttpResponse {
    // do something with payload
    HttpResponse::Ok().finish()
}

このように,「FromRequest を実装した型なら何を引数としても良いし,自分で実装したものを引数としても良い」という機能をユーザに選択肢を残すと表現したのです.

ケース8:マクロで型を自動生成する

ケース7とやや被りますが,マクロによって型を自動生成したいこともあります.

この具体例は diesel と呼ばれるクレートで,dieseltable! マクロによって users::tableusers::columns::id などの構造体を自動生成します.

これらの構造体はたとえば

use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};

users::table
    .count()
    .filter(users::id.eq(&id))
    .first(&conn)

のように使うのですが,ExpressionMethods トレイトと QueryDsl トレイト,RunQueryDsl トレイトの三つが出てきました.
これらのトレイトは,自動生成された構造体 users::table/users::id などに実装されています.

もちろん,自動生成された構造体を使おうと思えば,トレイトによらざるを得ません.
なぜなら,「トレイトを使わない」ということは「具体例な型名を代わりに用いる」ということであり,しかしライブラリの開発者にとっては,自動生成される予定の型名なんて分かるはずもないからです.

ケース9:複数の型を返す

さて,今まで述べてきたトレイトの使い方では,関数内で複数の型を返すことはできません.

それは,関数がアセンブリのレベルでどう実現されているかにも関連しますし,プログラミングの型理論とも関連しますが,いずれにせよ複数の型を返すことはできません.
Result enum や Either enum を使うという手もあるのですが,これらも万能ではありません.

という場合に有効なのがトレイトオブジェクト Box<dyn T> になります.詳しい使い方は公式ドキュメントへバトンタッチさせてください.

ケース10:構造体のフィールドや associated type などで複雑な・不明な型を用いる

ケース9で現れたトレイトオブジェクトの強力な応用の一つが,generics や impl Trait の使えない箇所で,複数な・不明な型を用いれることです.

たとえば次のコードでは generics も impl Trait も不当です:

struct MyIter<T> {
    inner: T,
}

impl<T: Iterator<Item = u64>> MyIter<T> {
    /// expected type parameter `T`
    ///            found struct `Map<std::vec::IntoIter<u64>, [closure@src/lib.rs:9:38: 9:47]>`
    fn new(v: Vec<u64>) -> Self {
        Self {
            inner: v.into_iter().map(|i| 2 * i),
        }
    }
}

Generics な MyIter<T> を返すべきところで,(generics でない)具体的な型 MyIter<Map<std::vec::IntoIter<u64>, ...>> を返してしまっているためです.

この場合ではきっとプログラムの設計を見直す方が良いのですが,どうしてもこのままでなければならないこともあります.
そういうときはトレイトオブジェクトを使って

struct MyIter {
    inner: Box<dyn Iterator<Item = u64>>,
}

impl MyIter {
    fn new(v: Vec<u64>) -> Self {
        Self {
            inner: Box::new(v.into_iter().map(|i| 2 * i)),
        }
    }
}

のようにできます.

この他に,Future トレイトのオブジェクト Box<dyn Future<Output = ...>> をよく見ます.
ケース7で挙げた actix web の FromRequest トレイトでも使われます.例として,web::Json 構造体の実装では

pub struct Json<T>(pub T);

impl<T> FromRequest for Json<T>
where
    T: DeserializeOwned + 'static,
{
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self, Error>>;
    type Config = JsonConfig;

    fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {}
}

のように,LocalBoxFuture が使われています.LocalBoxFuture<'_, T> は大雑把に言えば Pin<Box<dyn Future<Output = T>>> のエイリアスです.

Future トレイトは真面目に実装するのが大変なのでトレイトオブジェクトになっているのだと思います.

終わりに

以上,合わせて10ケース紹介してきました.

特にこの後半では,トレイトでしか実現できない機能ばかりになっています.
これを機にトレイトを使ってみよう!と思ってもらえるなら,とても嬉しいです.

また,ここに挙げた以外におもしろい使い方もあると思います.

それでは,良いトレイトライフを…….

Discussion