メインコンテンツまでスキップ

ジェネリクス

ジェネリクスを使用すると、異なる入力データ型に対して関数や構造体を定義できます。この言語機能は、パラメトリック多態性 として参照される事も有ります。Moveでは、ジェネリックという用語を型パラメーターや型引数と同じ意味でよく使用します。

ジェネリクスは、ベクターなどのライブラリコードでよく使用され、(指定された制約を満たす)あらゆるインスタンス化で機能するコードを宣言します。他のフレームワークでは、ジェネリクスコードを使用して、同じ実装を共有しながら、多数の異なる方法でグローバルストレージと対話す事が有ります。

型パラメーターを宣言する

関数と構造体は両方とも、シグネチャ内に山括弧<...>で囲まれた型パラメータのリストを取ることができます。

凡用(ジェネリクス)関数

関数の型パラメータは、関数名の後、(値)パラメータリストの前へ配置されます。次のコードは、汎用ID関数を定義し、任意の型の値を受け取り、その値を変更せず返します。

module 0x42::example {
fun id<T>(x: T): T {
// この型の注釈は不要ですが有効です
(x: T)
}
}

T型パラメータは定義されると、パラメータ型、戻り値の型、関数本体内で使用で出来ます。

ジェネリクス構造体

構造体の型パラメータは構造体名の後へ配置され、フィールドの型へ名前を付けるため使用出来ます。

module 0x42::example {
struct Foo<T> has copy, drop { x: T }

struct Bar<T1, T2> has copy, drop {
x: T1,
y: vector<T2>,
}
}

注意: 型パラメータを使用する必要は有りません

型引数

ジェネリクス関数を呼び出す

ジェネリクス関数を呼び出す場合、関数の型パラメータの型引数を山括弧で囲まれたリスト内で指定出来ます。

module 0x42::example {
fun foo() {
let x = id<bool>(true);
}
}

型引数を指定しない場合は、Moveの型推論が型引数を提供します。

ジェネリクス構造体を使う

ジェネリクス型の値を構築または破棄する時、構造体の型パラメータへ型引数のリストを添付する事も出来ます。

module 0x42::example {
fun foo() {
let foo = Foo<bool> { x: true };
let Foo<bool> { x } = foo;
}
}

型引数を指定しない場合は、Moveの型推論が型引数を提供します。

型引数の不一致

型引数を指定し、それが実際供給された値と競合する場合は、エラーが発生します。

module 0x42::example {
fun foo() {
let x = id<u64>(true); // エラー! trueはu64ではありません
}
}

同様に:

module 0x42::example {
fun foo() {
let foo = Foo<bool> { x: 0 }; // エラー! 0はboolではありません
let Foo<address> { x } = foo; // エラー! boolはアドレスと互換性がありません
}
}

型推論

殆どの場合、Moveコンパイラは型引数を推測できるため、明示的に記述する必要はありません。以下は上記の例の型引数を除外した場合です。

module 0x42::example {
fun foo() {
let x = id(true);
// ^ <bool>が推論されます

let foo = Foo { x: true };
// ^ <bool>が推論されます

let Foo { x } = foo;
// ^ <bool>が推論されます
}
}

注: コンパイラが型を推論できない場合は、手動で注釈を付ける必要があります。一般的なシナリオは、戻り位置にのみ型パラメータが現れる関数を呼び出す事です。

module 0x2::m {
use std::vector;

fun foo() {
// let v = vector::new();
// ^ コンパイラは要素の型を認識出来ません

let v = vector::new<u64>();
// ^~~~~ 手動で注釈を付ける必要が有ります
}
}

ただし、その戻り値が後でその関数内で使用される場合、コンパイラは型を推測する事が出来ます。

module 0x2::m {
use std::vector;

fun foo() {
let v = vector::new();
// ^ <u64>と推測されます
vector::push_back(&mut v, 42);
}
}

未使用の型パラメータ

構造体定義の場合、未使用の型パラメータは構造体で定義されたどのフィールドにも表示されませんが、コンパイル時、静的にチェックされます。Moveは未使用の型パラメータを許可するため、以下の構造体定義は有効です。

module 0x2::m {
struct Foo<T> {
foo: u64
}
}

これは、特定の概念をモデル化するときに便利です。以下の例を御覧下さい。

module 0x2::m {
// 通貨指定子
struct Currency1 {}
struct Currency2 {}

// 通貨指定子を使用してインスタンス化出来るジェネリクスコイン型
// specifier type.
//  例 Coin<Currency1>, Coin<Currency2> 等
struct Coin<Currency> has store {
value: u64
}

// 全ての通貨に関する一般的なコードを作成する
public fun mint_generic<Currency>(value: u64): Coin<Currency> {
Coin { value }
}

// ひとつの通貨に関する具体的なコードを作成する
public fun mint_concrete(value: u64): Coin<Currency1> {
Coin { value }
}
}

この例ではstruct Coin<Currency>Currency型パラメータに対して汎用的であり、コインの通貨を指定して、コードをどの通貨に対しても汎用的に記述出来、特定の通貨に対して具体的に記述する事も出来ます。この汎用性は、Coinで定義されたいずれのフィールドにもCurrency型パラメータが出現しない場合でも適用されます。

ファントム型パラメーター

上記の例ではstruct Coinstore機能を要求しますが、Coin<Currency1>Coin<Currency2>store機能を持ちません。これは条件付き機能とジェネリクス型のルールと、Currency1Currency2struct Coinの本体で使用されないにも関わらずstore機能を持たないという事実があるためです。これにより、好ましくない結果が生じる可能性があります。 例えば、グローバルストレージのウォレットにCoin<Currency1>を入れる事は出来ません。

考えられる解決策のひとつは、Currency1Currency2(即ちstruct Currency1 has store {})へ疑似機能注釈を追加する事です。 しかし、これは不必要な機能宣言によって型を弱めるため、バグやセキュリティの脆弱性へ繋がる可能性が有ります。

例えば、グローバルストレージ内のリソースにCurrency1型のフィールドが有る事は期待出来ませんが、疑似store機能を使用するとそれが可能となります。さらに、疑似注釈は感染性があり、必要な制約を含めるため、未使用の型パラメーターで多くの凡用的な関数が必要です。

ファントム型パラメータは、この問題を解決します。未使用の型パラメータは、構造体の機能の派生には関与しない ファントム 型パラメータとしてマーク出来ます。

このように、ファントム型パラメータへの引数は、ジェネリック型の機能を派生する時考慮されないため、擬似機能注釈の必要性が回避されます。

この緩やかなルールの適切さのためには、Moveの型システムはphantomとして宣言されたパラメータが構造体定義で全く使用されないか、phantomとして宣言された型パラメータへの引数としてのみ使用される事を保証します。

宣言

構造体定義では、型パラメータはその宣言の前にphantomキーワードを追加する事でファントムとして宣言出来ます。型パラメータがファントムとして宣言されている場合、それをファントム型パラメーターと言います。

構造体を定義する時Moveの型チェッカーは、全てのファントム型パラメータが、構造体定義内で使用されていない、もしくはファントム型パラメータへの引数としてのみ使用されている事を確認します。

より正式には、型がファントム型パラメータの引数として使用される場合、その型は ファントム位置 へ現れると言います。この定義を使用すると、ファントムパラメータの正しい使用規則は以下のように指出来ます。ファントム型パラメータはファントム位置でのみ表示出来ます

次の2つの例は、ファントムパラメータの有効な使用法を示しています。最初の例ではパラメータT1は構造体定義内で全く使用されていません。2つめの例では、パラメータT1はファントム型パラメータへの引数としてのみ使用されています。

module 0x2::m {
struct S1<phantom T1, T2> { f: u64 }
// ^^
// Ok: T1は構造体定義内へ表示されません

struct S2<phantom T1, T2> { f: S1<T1, T2> }
// ^^
// Ok: T1はファントム位置へ表示されます
}

次のコードはルール違反の例を示しています。

module 0x2::m {
struct S1<phantom T> { f: T }
// ^
// エラー: ファントム位置ではありません

struct S2<T> { f: T }

struct S3<phantom T> { f: S2<T> }
// ^
// エラー: ファントム位置ではありません
}

インスタンス化

構造体をインスタンス化する場合、構造体の機能を導出する時、ファントムパラメータへの引数は除外されます。例えば、以下のコードを考えてみます。

module 0x2::m {
struct S<T1, phantom T2> has copy { f: T1 }
struct NoCopy {}
struct HasCopy has copy {}
}

ここで、S<HasCopy, NoCopy>型について考えます。

Scopyで定義され、全ての非ファントム引数には copyがあるため、S<HasCopy, NoCopy>にもcopyが有ります。

機能制限付きファントム型パラメータ

機能制限とファントム型パラメータは、ファントムパラメータを機能制限で宣言できるという意味で、直角の機能です。機能制限を使用してファントム型パラメータをインスタンス化する場合、パラメータがファントムであっても、型引数はその制約を満たす必要があります。 例えば、以下の定義は完全に有効です。

module 0x2::m {
struct S<phantom T: copy> {}
}

通常の制限が適用され、Tcopyを持つ引数でのみインスタンス化出来ます。

制約

上記の例で、型パラメータを使用し呼び出し元が後でプラグイン出来る「不明な」型の定義方法を示しました。ただし、型システムが型に関する情報を殆ど持たず、非常に保守的な方法でチェックを実行する必要があります。ある意味で、型システムは制約の無いジェネリックの最悪のシナリオを想定する必要が有ります。簡単に言えば、デフォルトではジェネリック型パラメータには機能がありません。

ここで制約が登場します。制約は、これらの未知の型がどのような特性を持つかを指定する方法を提供し、それによって型システムは安全ではない操作以外の操作の許可が出来ます。

制約の宣言

以下の構文を使用し、型パラメータへ制約を課す事が出来ます。

// Tは型パラメーターの名前です 
T: <ability> (+ <ability>)*

<ability>は4つの機能のいずれかにする事が出来、型パラメータは複数の機能を一度に制約する事が出来ます。従って、以下は全て有効な型パラメータ宣言となります。

T: copy
T: copy + drop
T: copy + drop + store + key

制約の検証

制約は呼び出しサイトでチェックされるため、以下のコードはコンパイルされません。

module 0x2::m {
struct Foo<T: key> { x: T }

struct Bar { x: Foo<u8> }
// ^ エラー! u8には'key'が有りません

struct Baz<T> { x: Foo<T> }
// ^ エラー! Tには'key'が有りません
}
module 0x2::m {
struct R {}

fun unsafe_consume<T>(x: T) {
// エラー! xには'drop'が有りません
}

fun consume<T: drop>(x: T) {
// 有効!
// xは自動的に削除されます
}

fun foo() {
let r = R {};
consume<R>(r);
// ^ エラー! Rには'drop'が有りません
}
}
module 0x2::m {
struct R {}

fun unsafe_double<T>(x: T) {
(copy x, x)
// エラー! xには'copy'が有りません
}

fun double<T: copy>(x: T) {
(copy x, x) // 有効!
}

fun foo(): (R, R) {
let r = R {};
double<R>(r)
// ^ エラー! Rには'copy'が有りません
}
}

詳細は条件付き機能とジェネリック型の機能セクションを御覧下さい。

再帰の制限

再帰構造

ジェネリック構造体は、異なる型引数を持つ場合でも、直接的または間接的に同じ型のフィールドを含む事は出来ません。以下の構造体定義は全て無効です。

module 0x2::m {
struct Foo<T> {
x: Foo<u64> // エラー! 'Foo'が'Foo'を含んでいます
}

struct Bar<T> {
x: Bar<T> // エラー! 'Bar'が'Bar'を含んでいます
}

// エラー! 'A'と'B'がサイクルを形成していて、どちらも許可されません
struct A<T> {
x: B<T, u64>
}

struct B<T1, T2> {
x: A<T1>
y: A<T2>
}
}

高度なトピック: 型レベルの再帰

Moveを使用すると、ジェネリック関数を再帰的に呼び出す事が出来ます。ただし、ジェネリック構造体と組み合わせて使用​​すると、場合によっては無限の数の型が作成される可能性があり、これを許可する事は、コンパイラ、vm、その他の言語コンポーネントに不要な複雑さが追加される事を意味します。従って、このような再帰は禁止されています。

許可される:

module 0x2::m {
struct A<T> {}

// 有限の多数の型 -- 許可されます
// foo<T> -> foo<T> -> foo<T> -> ... 有効です
fun foo<T>() {
foo<T>();
}

// 有限の多数の型 -- 許可されます
// foo<T> -> foo<A<u64>> -> foo<A<u64>> -> ... 有効です
fun foo<T>() {
foo<A<u64>>();
}
}

許可されない:

module 0x2::m {
struct A<T> {}

// 無限の多数の型 -- 許可されません
// エラー!
// foo<T> -> foo<A<T>> -> foo<A<A<T>>> -> ...
fun foo<T>() {
foo<A<T>>();
}
}
module 0x2::n {
struct A<T> {}

// 無限の多数の型 -- 許可されません
// エラー!
// foo<T1, T2> -> bar<T2, T1> -> foo<T2, A<T1>>
// -> bar<A<T1>, T2> -> foo<A<T1>, A<T2>>
// -> bar<A<T2>, A<T1>> -> foo<A<T2>, A<A<T1>>>
// -> ...
fun foo<T1, T2>() {
bar<T2, T1>();
}

fun bar<T1, T2> {
foo<T1, A<T2>>();
}
}

注意:型レベルの再帰のチェックは、呼び出しサイトの保守的な分析に基づいており、制御フローやランタイム値は考慮されません。

module 0x2::m {
struct A<T> {}

fun foo<T>(n: u64) {
if (n > 0) {
foo<A<T>>(n - 1);
};
}
}

上記例の関数は、技術的には任意の入力で終了するため、有限多数の型のみを作成しますが、それでもMoveの型システムでは無効とみなされます。