関連型

関連型は、Rust型システムの強力な部分です。 関連型は、「型族」という概念と関連があり、言い換えると、複数の型をグループ化するものです。 この説明はすこし抽象的なので、実際の例を見ていきましょう。 例えば、 Graph トレイトを定義したいとしましょう、このときジェネリックになる2つの型: 頂点の型、辺の型 が存在します。 そのため、以下のように Graph<N, E> と書きたくなるでしょう:

fn main() { trait Graph<N, E> { fn has_edge(&self, &N, &N) -> bool; fn edges(&self, &N) -> Vec<E>; // etc } }
trait Graph<N, E> {
    fn has_edge(&self, &N, &N) -> bool;
    fn edges(&self, &N) -> Vec<E>;
    // etc
}

たしかに上のようなコードは動作しますが、この Graph の定義は少し扱いづらいです。 たとえば、任意の Graph を引数に取る関数は、 さらに 頂点 N と辺 E の型についてもジェネリックになる必要があります:

fn main() { fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> u32 { ... } }
fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> u32 { ... }

この距離を計算する関数distanceは、辺の型に関わらず動作します、そのためシグネチャに含まれる E に関連する部分は邪魔になります。

本当に表現したいのは、それぞれの Graph は、辺 E と頂点 N で構成されていることです。 それは、以下のように関連型を用いて表現できます:

fn main() { trait Graph { type N; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; // etc } }
trait Graph {
    type N;
    type E;

    fn has_edge(&self, &Self::N, &Self::N) -> bool;
    fn edges(&self, &Self::N) -> Vec<Self::E>;
    // etc
}

こうすると、使う側では、個々の Graph をより抽象的なものとして扱えます:

fn main() { fn distance<G: Graph>(graph: &G, start: &G::N, end: &G::N) -> u32 { ... } }
fn distance<G: Graph>(graph: &G, start: &G::N, end: &G::N) -> u32 { ... }

ここでは、頂点 E 型を扱わずに済んでいます!

もっと詳しく見ていきましょう。

関連型を定義する

早速、Graph トレイトを定義しましょう。以下がその定義です:

fn main() { trait Graph { type N; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; } }
trait Graph {
    type N;
    type E;

    fn has_edge(&self, &Self::N, &Self::N) -> bool;
    fn edges(&self, &Self::N) -> Vec<Self::E>;
}

非常にシンプルですね。関連型には type キーワードを使い、そしてトレイトの本体にある関数で利用します。

これらの type 宣言は、関数で利用できるものと同じものが全て利用できます。 たとえば、頂点を表示するため N 型には Display を実装してほしいなら、以下のように指定できます:

fn main() { use std::fmt; trait Graph { type N: fmt::Display; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; } }
use std::fmt;

trait Graph {
    type N: fmt::Display;
    type E;

    fn has_edge(&self, &Self::N, &Self::N) -> bool;
    fn edges(&self, &Self::N) -> Vec<Self::E>;
}

関連型を実装する

通常のトレイトと同様に、関連型を使っているトレイトは実装するために impl を利用します。 以下は、シンプルなGraphの実装例です:

fn main() { trait Graph { type N; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; } struct Node; struct Edge; struct MyGraph; impl Graph for MyGraph { type N = Node; type E = Edge; fn has_edge(&self, n1: &Node, n2: &Node) -> bool { true } fn edges(&self, n: &Node) -> Vec<Edge> { Vec::new() } } }
struct Node;

struct Edge;

struct MyGraph;

impl Graph for MyGraph {
    type N = Node;
    type E = Edge;

    fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
        true
    }

    fn edges(&self, n: &Node) -> Vec<Edge> {
        Vec::new()
    }
}

この、いささか単純過ぎる実装では、常に true と空の Vec<Edge> を返します。 しかし、関連型をどう定義したらよいのかを教えてくれます。 まず、はじめに3つの struct が必要です。 グラフのためにひとつ、頂点のためにひとつ、辺のためにひとつです。 もし異なる型を利用するのが適切ならば、そうしても構いません。 今回はこの3つの struct を用います。

次は impl の行です。 これは他のトレイトを実装するときと同様です。

そして、= を関連型を定義するために利用します。 トレイトが利用する名前は = の左側にある名前で、実装に用いる具体的な型は右側にあるものになります。 最後に、具体的な型を関数の宣言に利用します。

関連型を伴うトレイト

すこし触れておきたい構文のひとつに、トレイトオブジェクトがあります。 もし、トレイトオブジェクトを以下のように関連型を持つトレイトから作成しようとした場合:

fn main() { trait Graph { type N; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; } struct Node; struct Edge; struct MyGraph; impl Graph for MyGraph { type N = Node; type E = Edge; fn has_edge(&self, n1: &Node, n2: &Node) -> bool { true } fn edges(&self, n: &Node) -> Vec<Edge> { Vec::new() } } let graph = MyGraph; let obj = Box::new(graph) as Box<Graph>; }
let graph = MyGraph;
let obj = Box::new(graph) as Box<Graph>;

以下の様なエラーが発生します:

error: the value of the associated type `E` (from the trait `main::Graph`) must
be specified [E0191]
let obj = Box::new(graph) as Box<Graph>;
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24:44 error: the value of the associated type `N` (from the trait
`main::Graph`) must be specified [E0191]
let obj = Box::new(graph) as Box<Graph>;
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~

上のようにしてトレイトオブジェクトを作ることはできません。 なぜなら関連型について知らないからです。 代わりに以下のように書けます:

fn main() { trait Graph { type N; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; } struct Node; struct Edge; struct MyGraph; impl Graph for MyGraph { type N = Node; type E = Edge; fn has_edge(&self, n1: &Node, n2: &Node) -> bool { true } fn edges(&self, n: &Node) -> Vec<Edge> { Vec::new() } } let graph = MyGraph; let obj = Box::new(graph) as Box<Graph<N=Node, E=Edge>>; }
let graph = MyGraph;
let obj = Box::new(graph) as Box<Graph<N=Node, E=Edge>>;

N=Node 構文を用いて型パラメータ N に対して具体的な型 Node を指定できます。 E=Edge についても同様です。 もしこの制約を指定しなかった場合、このトレイトオブジェクトに対してどの impl がマッチするのか定まりません。