ジェネリクスは、任意の型で動作する型や関数を定義する方法です。異なる型で使用できる関数を作成したい場合や、任意の型を保持できる型を定義したい場合に便利です。ジェネリクスは、コレクション、抽象実装など、Moveの多くの高度な機能の基盤となっています。

標準ライブラリ

この章ではすでにベクタ型 [TODO] について触れましたが、これは任意の型を保持できるジェネリック型です。標準ライブラリの別の例としては、値が存在するかもしれないし、存在しないかもしれないという状況を表すために使用されるOption型 [TODO] があります。

ジェネリック構文

ジェネリック型や関数を定義するには、角括弧(<>)で囲まれたジェネリックパラメータのリストが型シグネチャに必要です。ジェネリックパラメータはカンマで区切られます。

/// 任意の型 `T` のコンテナ。
public struct Container<T> has drop {
    value: T,
}

/// ジェネリック値 `T` を持つ新しい `Container` を作成する関数
public fun new<T>(value: T): Container<T> {
    Container { value }
}

上の例では、Containerは単一の型パラメータTを持つジェネリック型で、コンテナのvalueフィールドはTを格納します。new関数は単一の型パラメータTを持つジェネリック関数で、与えられた値を持つContainerを返します。ジェネリック型は具体的な型で初期化する必要があり、ジェネリック関数は具体的な型で呼び出す必要があります。

#[test]
fun test_generic() {
    // これら3行は等価
    let container: Container<u8> = new(10); // 型推論
    let container = new<u8>(10); // `u8`値を持つ新しい `Container` を作成
    let container = new(10u8);

    assert!(container.value == 10, 0x0);

    // 値は `drop` 能力を持つ場合に限り無視できます
    let Container { value: _ } = container;
}

test_generic 関数で、u8値を持つ新しい Container を作成する3つの等価な方法を示しています。数値型は推論が必要なため、数値リテラルの型を指定します。

複数の型パラメータ

型や関数を複数の型パラメータで定義することができます。型パラメータはカンマで区切られます。

/// 任意の型 `T` と `U` の値のペア
public struct Pair<T, U> {
    first: T,
    second: U,
}

/// 二つのジェネリック値 `T` と `U` を持つ新しい `Pair` を作成する関数
public fun new_pair<T, U>(first: T, second: U): Pair<T, U> {
    Pair { first, second }
}

上の例では、Pairは二つの型パラメータTUを持つジェネリック型で、new_pair関数は二つの型パラメータTUを持つジェネリック関数です。この関数は与えられた値を持つPairを返します。型パラメータの順序は重要であり、型シグネチャの型パラメータの順序と一致する必要があります。

#[test]
fun test_generic() {
    // これら3行は等価
    let pair_1: Pair<u8, bool> = new_pair(10, true); // 型推論
    let pair_2 = new_pair<u8, bool>(10, true); // `u8`と`bool`値を持つ新しい `Pair` を作成
    let pair_3 = new_pair(10u8, true);

    assert!(pair_1.first == 10, 0x0);
    assert!(pair_1.second, 0x0);

    // アンパックは同一
    let Pair { first: _, second: _ } = pair_1;
    let Pair { first: _, second: _ } = pair_2;
    let Pair { first: _, second: _ } = pair_3;
}

もしnew_pair関数で型パラメータを交換した別のインスタンスを追加し、二つの型を比較しようとした場合、型シグネチャが異なるため比較できないことがわかります。

#[test]
fun test_swap_type_params() {
    let pair1: Pair<u8, bool> = new_pair(10u8, true);
    let pair2: Pair<bool, u8> = new_pair(true, 10u8);

    // この行はコンパイルされません
    // assert!(pair1 == pair2, 0x0);

    let Pair { first: pf1, second: ps1 } = pair1; // first1: u8, second1: bool
    let Pair { first: pf2, second: ps2 } = pair2; // first2: bool, second2: u8

    assert!(pf1 == ps2, 0x0); // 10 == 10
    assert!(ps1 == pf2, 0x0); // true == true
}

変数pair1pair2の型は異なり、比較はコンパイルされません。

ジェネリクスの利点

上述した例では、ジェネリック型のインスタンス化やジェネリック関数の呼び出しに焦点を当てていますが、ジェネリクスの真の力は、基本となるジェネリック型に共通の振る舞いを定義し、具体的な型に依存せずにそれを使用する能力にあります。これは、コレクションや抽象実装などの高度な機能で作業する際に特に有用です。

/// 名前、年齢、およびいくつかのジェネリックメタデータを持つユーザーレコード
public struct User<T> {
    name: String,
    age: u8,
    /// アプリケーションによって異なる
    metadata: T,
}