🦀

100日後にRustをちょっと知ってる人になる: [Day 40]関連型 (associated type)

2022/10/06に公開

Day 40 のテーマ

Day 33 でジェネリクスに関する基本内容を確認しました。

以下のように、ダイヤモンド演算子 <> を使い、ジェネリック型の名前を指定することで定義する事ができることを確認していました。

struct Point<T> { x: T, y: T }

impl<T> Point<T> {
    fn do_something(self) -> (T, T) {
        (self.x, self.y)
    }
}

また、ジェネリック型の対して特定の処理 (トレイト) を実装しなければならないという事ができることを確認していました。その方式を トレイト境界と呼び、以下のように定義することを確認していました。

fn printer<T: Display>(t: T) {
    println!("{}", t);
}

上記の例では、Displya トレイトを実装している T 型をパラメータとして受け取る printer 関数が定義されています。つまり、println! マクロでパラメータを表示させようとしていますが、T が指定されたトレイトを実装していない場合はコンパイルエラーになってしまいます。

このように、ジェネリクスをつかってプログラムをコンパクトに書きつつも規定をかけていくことができることを確認できていました。

今回はジェネリクスを使って便利にすることができる、**Associated Type (関連型)**について考えてみたいと思います。

関連型

関連型を使っていない場合

まず最初に次のケースでのジェネリクスの利用について考えてみてください。

  • あるトレイトがジェネリック型を用いて定義されている場合

この場合、このトレイトが実装されたものを使用する場合どうなるでしょうか?

実装例を見ながら考えてみましょう。

  • 構造体
// 32 bit の整数型の要素を2つもつ構造体
struct Point(i32, i32);
  • トレイト
// Point の座標があっているかどうかを確認するトレイト
trait Position<X, Y> {
    fn exist(&self, _: &X, _: &Y) -> bool;
    fn h_axis(&self) -> i32;
    fn v_axis(&self) -> i32;
}
  • 実装
impl Position<i32, i32> for Point {
    // 2つの要素が正しいことを確認
    fn exist(&self, x: &i32, y: &i32) -> bool {
        (&self.0 == x) && (&self.1 == y)
    }

    // x座標を取得
    fn h_axis(&self) -> i32 {
        self.0
    }

    // y座標を取得
    fn v_axis(&self) -> i32 {
        self.1
    }
}
  • main 関数
fn main() {
    let x = 5;
    let y = 10;

    let point = Point(x, y);

    println!("Point X:{}, Y:{}", &x, &y);
    println!("Exist?:{}", point.exist(&x, &y));

    println!("Point-X:{}", point.v_axis());
    println!("Point-X:{}", point.h_axis());
}

ここまでで、一旦動くようになります。
ですが、次のことを考えてみてください。

トレイト境界として Position を制約された引数をもつ関数がある場合はどうなるでしょうか?
次のように、XYZ に含まれているにも関わらず、XY を 2 回書いています。

fn new_point<X, Y, Z>(point: &Z) where Z: Position<X, Y> {...}

冗長なので、できれば 2 回は書きたくないですよね。

こういう場合に関連型を使うと便利になります。

関連型を使う場合

関連型を使うと、トレイトの中に アウトプット型 として書くことにより可読性を向上させることができます。

次のようにトレイトを定義します。
ポイントとしては、XYtype キーワードを使ってトレイト内部で定義するようにしています

trait Position {
    type X;
    type Y;

    fn exist(&self, _: &Self::X, _: &Self::Y) -> bool;
    fn h_axis(&self) -> i32;
    fn v_axis(&self) -> i32;
}

そして、使う側の関数は次のように、明示的に XY の定義が不要となります。

fn new_point<Z: Position>(point: &Z) {
    println!("POINT:({},{})", point.v_axis(), point.h_axis())
}

関連型とジェネリクス

関連型とジェネリクスによる両方の実装についてみてみました。
いずれの場合でも処理内容は変わらず実装ができるのは言うまでもないですよね。
どういうときに使い分けるのがよいのかという点について考えてみました。

  • 関連型
    • 「トレイト」 : 「実装対象の型 (Self)」 = 1 : 1
  • ジェネリクス
    • 「トレイト」 : 「実装対象の型 (Self)」 = N : 1

つまり、ある型に対して別の型が一意に定まる場合は関連型を使うのがいいということになりそうです。

Day 40 のまとめ

ジェネリクスの使い方はどのようなプログラム言語でも大事だと思います。Rust でも同様にジェネリクスをうまく使うことでより綺麗なコードが書けるということが分かってきました。

汎用性を高めるためにジェネリクスが使われるわけですが、汎用性を高める反面で可読性が落ちてしまうケースがありえると今回分かりました。冗長的なコードになるようなケースです。
トレイトと実装対象の関係が予め明確な場合は、関連型を用いてよりシンプルに定義するということがよいのかなと今回思いました。

完走としては、トレイトや関連型の関係をもっと理解し、可読性が高く効率的なコードが書けるようにもっと学びたいと思います。

GitHubで編集を提案

Discussion