🫥

ジェネリクスを用いた関数の実装

2023/10/20に公開

ジェネリクスとトレイト境界

ある関数を複数の型に対して実装したい時にimpl Traitを使用した静的ディスパッチがあるが、ジェネリクスを使用することでも静的ディスパッチを行うこともできる。この様に、「あるトレイトを実装しているか」という条件をトレイト境界と呼ぶ。

つまり?

ジェネリクスで関数を定義すると一つの関数を複数の型に対して使用することができ実装を減らすことができる。

ジェネリクス無しでの実装

例えば、以下の様な二次元と三次元ベクトルを表す構造体とそれのノルムを比較する実装があるとする。
もっと簡単に実装することもできるが今回は全ての実装を行う。

use std::cmp::Ordering;

struct Vector2D {
    x: i32,
    y: i32,
}

impl Vector2D {
    fn new(x: i32, y: i32) -> Self {
        Self { x, y }
    }
    fn norm(&self) -> i32 {
        self.x * self.x + self.y * self.y
    }
}

impl PartialEq for Vector2D {
    fn eq(&self, other: &Self) -> bool {
        self.norm() == other.norm()
    }
}

impl PartialOrd for Vector2D {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Vector2D {
    fn cmp(&self, other: &Self) -> Ordering {
        self.norm().cmp(&other.norm())
    }
}

struct Vector3D {
    x: i32,
    y: i32,
    z: i32,
}

impl Vector3D {
    fn new(x: i32, y: i32, z: i32) -> Self {
        Self { x, y, z }
    }
    fn norm(&self) -> i32 {
        self.x * self.x + self.y * self.y + self.z * self.z
    }
}

impl PartialEq for Vector3D {
    fn eq(&self, other: &Self) -> bool {
        self.norm() == other.norm()
    }
}

impl PartialOrd for Vector3D {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Vector3D {
    fn cmp(&self, other: &Self) -> Ordering {
        self.norm().cmp(&other.norm())
    }
}

これを用いて、この構造体の配列からノルムが最大の要素を求める関数を実装するとする。

fn max(array: &[Vector2D]) -> &Vector2D {
    let mut max = array[0];
    for i in 1..array.len() {
        if array[i] > *max {
            max = array[i];
        }
    }
    max
}

この関数は正しく機能するが、Vector2D型にしか対応していない。もし、Vector3D型にも対応したい場合は、それぞれの型に対してmax関数を実装する必要がある。

fn main() {
    let array = [
        Vector3D::new(1, 2, 3),
        Vector3D::new(4, 5, 6),
        Vector3D::new(7, 8, 9),
    ];
    println!("{:?}", max(&array)); // error
}

ジェネリクスを使用した実装

ジェネリクスを使用することで、Vector2D型とVector3D型に対して同じ関数を使用することができる。

+ fn max<T: Ord>(array: &[T]) -> &T {
    let mut max = &array[0];
    for i in 1..array.len() {
        if array[i] > *max {
            max = &array[i];
        }
    }
    max
}

解説

<T: Ord>?

Tはジェネリクスの型パラメータで、任意のタイプを表す。
なのでarray: &[T]T型のスライスを表し、&TT型の参照を表す。

Ord?

T型はOrdトレイトを実装している必要があるという意味。今回のVector2DVector3DOrdトレイトを実装しているので条件を満たしている。

おまけ

ジェネリクスについて

where句を使用することでも同じことができる。

- fn max<T: Ord>(array: &[T]) -> &T {
+ fn max<T>(array: &[T]) -> &T where T: Ord {
    let mut max = &array[0];
    for i in 1..array.len() {
        if array[i] > *max {
            max = &array[i];
        }
    }
    max
}

複数のトレイトを要求する時は+を使用する。

fn max<T>(array: &[T]) -> &T
where
    T: Ord + Display,
{
    let mut max = &array[0];
    for i in 1..array.len() {
        if array[i] > *max {
            max = &array[i];
        }
    }
    println!("{}", max); // Displayトレイトを実装しているので使用できる
}

Traitの実装の自動化

deriveマクロを使用することで、PartialEqPartialOrdなどのトレイトを自動で実装することができる。

+ #[derive(PartialEq, PartialOrd, Ord)]
struct Vector2D {
    x: i32,
    y: i32,
}

これは非常に便利なので積極的に利用していきたい。ただし、自作のトレイトをderiveマクロに対応させるのは中々難しいので後日説明する。

Discussion