ジェネリクス

時々、関数やデータ型を書いていると、引数が複数の型に対応したものが欲しくなることもあります。Rustでは、ジェネリクスを用いてこれを実現しています。ジェネリクスは型理論において「パラメトリック多相」(parametric polymorphism)と呼ばれ、与えられたパラメータにより(「parametric」)型もしくは関数が多くの相(「poly」は「多くの」、「morph」は「相(かたち)」を意味します)(訳注: ここで「相」は型を指します)を持つことを意味しています。

さて、型理論はもう十分です。続いてジェネリックなコードを幾つか見ていきましょう。Rustが標準ライブラリで提供している型 Option<T> はジェネリックです。

fn main() { enum Option<T> { Some(T), None, } }
enum Option<T> {
    Some(T),
    None,
}

<T> の部分は、前に少し見たことがあると思いますが、これがジェネリックなデータ型であることを示しています。 enum の宣言内であれば、どこでも T を使うことができ、宣言内に登場する同じ型をジェネリック内で T 型に置き換えています。型注釈を用いたOption<T>の使用例が以下になります。

fn main() { let x: Option<i32> = Some(5); }
let x: Option<i32> = Some(5);

この型宣言では Option<i32> と書かれています。 Option<T> の違いに注目して下さい。そう、上記の Option では T の値は i32 です。この束縛の右辺の Some(T) では、 T5 となります。それが i32 なので、両辺の型が一致するため、Rustは満足します。型が不一致であれば、以下のようなエラーが発生します。

fn main() { let x: Option<f64> = Some(5); // error: mismatched types: expected `core::option::Option<f64>`, // found `core::option::Option<_>` (expected f64 but found integral variable) }
let x: Option<f64> = Some(5);
// error: mismatched types: expected `core::option::Option<f64>`,
// found `core::option::Option<_>` (expected f64 but found integral variable)

これは f64 を保持する Option<T> が作れないという意味ではありませんからね!リテラルと宣言の型をぴったり合わせなければなりません。

fn main() { let x: Option<i32> = Some(5); let y: Option<f64> = Some(5.0f64); }
let x: Option<i32> = Some(5);
let y: Option<f64> = Some(5.0f64);

これだけで結構です。1つの定義で、多くの用途が得られます。

ジェネリクスにおいてジェネリックな型は1つまで、といった制限はありません。Rustの標準ライブラリに入っている類似の型 Result<T, E> について考えてみます。

fn main() { enum Result<T, E> { Ok(T), Err(E), } }
enum Result<T, E> {
    Ok(T),
    Err(E),
}

この型では TE2つ がジェネリックです。ちなみに、大文字の部分はあなたの好きな文字で構いません。もしあなたが望むなら Result<T, E> を、

fn main() { enum Result<A, Z> { Ok(A), Err(Z), } }
enum Result<A, Z> {
    Ok(A),
    Err(Z),
}

のように定義できます。慣習としては、「Type」から第1ジェネリックパラメータは T であるべきですし、「Error」から E を用いるのですが、Rustは気にしません。

Result<T, E> 型は計算の結果を返すために使われることが想定されており、正常に動作しなかった場合にエラーの値を返す機能を持っています。

ジェネリック関数

似た構文でジェネリックな型を取る関数を記述できます。

fn main() { fn takes_anything<T>(x: T) { // do something with x // xで何か行う } }
fn takes_anything<T>(x: T) {
  // xで何か行う
}

構文は2つのパーツから成ります。 <T> は「この関数は1つの型、 T に対してジェネリックである」ということであり、 x: T は「xは T 型である」という意味です。

複数の引数が同じジェネリックな型を持つこともできます。

fn main() { fn takes_two_of_the_same_things<T>(x: T, y: T) { // ... } }
fn takes_two_of_the_same_things<T>(x: T, y: T) {
    // ...
}

複数の型を取るバージョンを記述することも可能です。

fn main() { fn takes_two_things<T, U>(x: T, y: U) { // ... } }
fn takes_two_things<T, U>(x: T, y: U) {
    // ...
}

ジェネリック構造体

また、 struct 内にジェネリックな型の値を保存することもできます。

fn main() { struct Point<T> { x: T, y: T, } let int_origin = Point { x: 0, y: 0 }; let float_origin = Point { x: 0.0, y: 0.0 }; }
struct Point<T> {
    x: T,
    y: T,
}

let int_origin = Point { x: 0, y: 0 };
let float_origin = Point { x: 0.0, y: 0.0 };

関数と同様に、 <T> がジェネリックパラメータを宣言する場所であり、型宣言において x: T を使うのも同じです。

ジェネリックな struct に実装を追加したい場合、 impl の後に型パラメータを宣言するだけです。

fn main() { struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn swap(&mut self) { std::mem::swap(&mut self.x, &mut self.y); } } }
impl<T> Point<T> {
    fn swap(&mut self) {
        std::mem::swap(&mut self.x, &mut self.y);
    }
}

ここまででありとあらゆる型をとることのできるジェネリクスについて見てきました。多くの場合これらは有用です。 Option<T> は既に見た通りですし、のちに Vec<T> のような普遍的なコンテナ型を知ることになるでしょう。一方で、その柔軟性と引き換えに表現力を増加させたくなることもあります。それは何故か、そしてその方法を知るためには トレイト境界 を読んで下さい。