Rustの関連型の使いどころ

κeenです。昨日はRustのLT会! Rust入門者の集い - connpassに参加してきました。 そこで関連型に関する発表があったので感化されて私も一筆。

馴れないと関連型はジェネリクスでいいじゃんと思えますが、両者は別の機能を提供するものです。 設計による使い分けではなくて実現したいことに応じた機能による使い分けをするので馴れてしまえば迷うことなくどちらを使うか判断出来ます。

ということで関連型のパターンをいくつか。 もちろん、根底にある関連型という機能は共通なのでほとんど同じようなことを言ってますが気持としてパターンを知っておくと便利です。

型レベルの関数として

「関連」型なのである型に関連する型を表現します。ちょっと見方を変えると型から型への写像、つまり関数になります。 例えばこういうのです。

trait ToUnsigned {
    type Counterpart;
    fn to_unsigned(self) -> Self::Counterpart;
}

impl ToUnsigned for i32 {
    type Counterpart = u32;
    fn to_unsigned(self) -> Self::Counterpart {
        self as u32
    }
}

i32 から u32 への関数になってそうなのが見えますかね?もちろん、 i64 から u64 などへの対応も作れます。 こういうのは例えば符号無し数にのみ演算が定義されている場合とかに便利ですね。

fn write_bigendian_signed<I, U>(i: I) -> ()
    where I: ToUnsigned<Counterpart = U>,
          U: ...,  {
}

トレイト内で使う型を固定するため

これが一番目にするやつじゃないでしょうか。

trait Handler {
    type Request;
    type Response;
    fn handle(req: Self::Request, res: Self::Response) -> io::Result<()>;
}

struct HTTPHandler;

impl Handler for HTTPHandler {
    type Request = HTTPRequest;
    type Response = HTTPResponse;
    fn handle(req: Self::Request, res: Self::Response) -> io::Result<()>;
}

固定というか特殊化というか、トレイト自体は汎用的に作られていて、それを実装する型が特定の処理に特化するパターンです。 ジェネリクスで受け取る訳にはいかなくて、実装すべき型をトレイトの中で定義してあげる必要があるのは分かると思います。

関数の返り値を一般化するため

これは現在のRust(rust-1.13.0)が impl Traitをサポートしていないために必要になるテクニックです。 関連型とトレイト境界を組み合わせて使います。

trait ReverseIter {
    type Item;
    type Iter: Iterator<Item = Self::Item>;
    fn rev_iter(&self) -> Iter;
}

これの type Iter: Iterator<Item = Self::Item>; の方です。 返り値のIteratorを抽象化したいのですが、現在のRustではfn rev_iter(&self) -> Iterator<Item = Self::Item>;のような書き方が出来ないので仕方なく関連型を使ってあげる必要があります。

ちょっと踏み込んだ話をすると、関数の引数の多相性は∀の量化、返り値の多相は∃の量化です。そしてトレイトのジェネリクスも∀の量化で関連型が∃の量化なのでそういう対応がある訳です。

最後に

結構理論的にも色々あるようなので調べてみると様々なブログポストが見付かると思います。

ぱぱっと思いついたパターンを3つ挙げました。もしかしたら他にもパターンがあるかもしれません。

これを知っておけばtokio-serviceServiceみたいな関連型を多用するパターンでもひるまなくなります。

参考

関連型とimpl Traitに関して

応用

発展的な話題

Written by κeen