自己紹介
- @tacke_jp
- 株式会社ノハナでエンジニアをしています
- 今はParse.com終了にサービスが道連れにならないように移行作業をしています
- 趣味でHaskellとかRustとか触っています
- 今日はRustを触り始めて興味を持った "Associated Type" についての話をします
Associated Type (関連型) is 何?
- Traitに紐づく型パラメータ
- Traitを実装する型に対して一意に定まる
(型に関連付く型 = "Associated")
- Traitを実装する型に対して一意に定まる
- 他の言語だとHaskellやSwiftなどが対応してる
- 概念としては Type Family (型族) に近いらしい
- 詳しい人教えてください😭
例
グラフを表現するtraitを考える。
trait Graph<N, E> {
fn has_edge(&self, &N, &N) -> bool;
fn edges(&self, &N) -> Vec<E>;
}
(公式ドキュメントより)
実装例
struct MyGraph;
struct Node;
struct Edge;
impl Graph<Node, Edge> for MyGraph {
fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
true
}
fn edges(&self, n: &Node) -> Vec<Edge> {
Vec::new()
}
}
これに対してグラフの2点間の距離を計算する関数distance
を定義することを考える。
fn distance<N, E, G: Graph<N, E>>
(graph: &G, start: &N, end: &N)
-> u32 { ... }
問題点
- シグネチャの表現が冗長で誤解を招きやすい
- 任意の
N
やE
を取れるかのように見える - 実際は
impl Graph<>
時に定義されたものしか使えない
- 任意の
- 型パラメータが増えたときに辛い
- 例えば
<A, B, C, D, E, F, G: Graph<A, B, C, D, E, F>>
😨 - これを
Graph
を引数や戻り値として使う関数ごとに書かないといけないとなると...
- 例えば
Associated Typeを使ってみる
trait Graph {
type N;
type E;
fn has_edge(&self, &Self::N, &Self::N) -> bool;
fn edges(&self, &Self::N) -> Vec<Self::E>;
}
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()
}
}
関数distance
を定義してみる
fn distance<G: Graph>
(graph: &G, start: &G::N, end: &G::N)
-> u32 { ... }
すっきり書けた🙆
型消去する場合はこうする。
let graph = MyGraph;
let graphs = Vec::new()
as Vec<Graph<<N=Node, E=Edge>>;
graphs.add(graph);
(型消去すると N
と E
が一意に定まらないので、具体的に指定してやる必要がある。)
メリット
- 余分な型パラメータを宣言する必要がなく可読性が高い
- 型パラメータの追加/削除などの変更に強い
- シグネチャを書き直さなくても済む
- ライブラリ作者がAssociated Typeにしておいてくれると使い手が楽できる
注意点
- 型を代入しなくて良くなる訳ではない
- 関数のシグネチャで
<G: Graph>
と型パラメータを省略できるが裏では代入されている -
let g: Graph
という変数を宣言することは出来ない
- 関数のシグネチャで
- 型の表現力が増すわけではない
- 同等の表現はGenericsを使っても書ける
=> あくまで型レベルでのプログラミングを楽にするための機能
おまけ
Rustでこう書きたいけど書けない。
trait Graph {
type N;
type E;
fn compare<G: Graph>(&self, other: G)
-> bool
where Self.N == G.N
, Self.E == G.E;
}
-
error: equality constraints are not yet supported in where clauses (#20041)
と怒られる😇- Genericsなら
fn compare<G: Graph<N, E>>(&self, other: G) -> bool;
こう書ける
- Genericsなら
ちなみにSwiftだと書ける。そのうち対応されると良いな...
おまけ(その2)
termoshtt さんにコメントをいただきました。
(ありがとうございます!)
traitを使って型レベルでイコールを作るコードを紹介いただきました。
trait Equal<A> {}
impl<A> Equal<A> for A {}
trait Graph {
type N;
type E;
fn compare<G: Graph>(&self, other: G) -> bool
where Self::N: Equal<G::N>,
Self::E: Equal<G::E>;
}
おまけ(その3)
blackenedgoldさんにもコメント頂きました。(ありがとうございます!)
Self::N
Self::E
を代入してしまえば良いんですね...!
trait Graph {
type N;
type E;
fn compare<G: Graph<N = Self::N, E = Self::E>>(&self, other: G)
-> bool;
}
まとめ
- Genericsで書いていたところをAssociated Typeで書くと簡潔になる
- Genericsを使う際の便利な補助機能だと思って使えば良さそう
- 型かわいいよ型