The Go Blog
型パラメータの分解
slicesパッケージの関数シグネチャ
slices.Clone関数は非常にシンプルで、あらゆる型のスライスをコピーします。
func Clone[S ~[]E, E any](s S) S {
return append(s[:0:0], s...)
}
これは、容量がゼロのスライスに要素を追加すると、新しいバッキング配列が割り当てられるためです。関数本体は関数シグネチャよりも短くなりますが、これは本体が短いこともありますが、シグネチャが長いことも一因です。このブログ記事では、なぜシグネチャがそのように記述されているのかを説明します。
シンプルなクローン
まず、シンプルなジェネリックなClone関数を記述してみましょう。これはslicesパッケージのものではありません。あらゆる要素型のスライスを受け取り、新しいスライスを返すことを目的とします。
func Clone1[E any](s []E) []E {
// body omitted
}
ジェネリック関数Clone1は、単一の型パラメータEを持ちます。E型のスライスである単一の引数sを受け取り、同じ型のスライスを返します。このシグネチャは、Goのジェネリクスに慣れている人にとっては簡単です。
しかし、問題があります。名前付きスライス型はGoでは一般的ではありませんが、人々はそれらを使用します。
// MySlice is a slice of strings with a special String method.
type MySlice []string
// String returns the printable version of a MySlice value.
func (s MySlice) String() string {
return strings.Join(s, "+")
}
MySliceのコピーを作成し、ソートされた文字列を含む印刷可能なバージョンを取得したいとしましょう。
func PrintSorted(ms MySlice) string {
c := Clone1(ms)
slices.Sort(c)
return c.String() // FAILS TO COMPILE
}
残念ながら、これはうまくいきません。コンパイラはエラーを報告します。
c.String undefined (type []string has no field or method String)
型パラメータを型引数で置き換えてClone1を手動でインスタンス化すると、問題がわかります。
func InstantiatedClone1(s []string) []string
Goの代入ルールでは、MySlice型の値を[]string型のパラメータに渡すことができるため、Clone1の呼び出しは問題ありません。しかし、Clone1は[]string型の値を返し、MySlice型の値を返しません。[]string型にはStringメソッドがないため、コンパイラはエラーを報告します。
柔軟なクローン
この問題を解決するには、引数と同じ型を返すCloneのバージョンを記述する必要があります。それができれば、MySlice型の値でCloneを呼び出したときに、MySlice型の結果が返されます。
このように見えるはずだとわかっています。
func Clone2[S ?](s S) S // INVALID
このClone2関数は、引数と同じ型の値を返します。
ここでは制約を?と記述しましたが、これは単なるプレースホルダーです。これを機能させるには、関数の本体を記述できる制約を記述する必要があります。Clone1では、要素型にanyの制約を使用するだけで済みました。Clone2ではそれが機能しません。sがスライス型であることを要求したいからです。
スライスが必要であることがわかっているので、Sの制約はスライスである必要があります。スライスの要素型は何でも構わないので、Clone1で行ったように、それをEと呼びましょう。
func Clone3[S []E](s S) S // INVALID
これはまだ無効です。Eを宣言していないからです。Eの型引数は任意の型であるため、それ自体も型パラメータである必要があります。任意の型であるため、その制約はanyです。
func Clone4[S []E, E any](s S) S
これは近づいており、少なくともコンパイルはできますが、まだ完全にではありません。このバージョンをコンパイルすると、Clone4(ms)を呼び出したときにエラーが発生します。
MySlice does not satisfy []string (possibly missing ~ for []string in []string)
コンパイラは、MySliceが制約[]Eを満たさないため、型パラメータSに型引数MySliceを使用できないと伝えています。これは、制約としての[]Eが[]stringのようなスライス型リテラルのみを許可し、MySliceのような名前付き型を許可しないためです。
基底型の制約
エラーメッセージが示唆するように、答えは~を追加することです。
func Clone5[S ~[]E, E any](s S) S
繰り返しますが、型パラメータと制約[S []E, E any]を記述することは、Sの型引数が任意の無名スライス型であることを意味しますが、スライスリテラルとして定義された名前付き型ではありえません。~付きで[S ~[]E, E any]と記述することは、Sの型引数が、その基底型がスライス型である任意の型であることを意味します。
任意の名前付き型type T1 T2の場合、T1の基底型はT2の基底型です。intのような事前宣言された型や[]stringのような型リテラルの基底型は、型そのものです。詳細については、言語仕様を参照してください。例では、MySliceの基底型は[]stringです。
MySliceの基底型はスライスなので、MySlice型の引数をClone5に渡すことができます。お気づきかもしれませんが、Clone5のシグネチャはslices.Cloneのシグネチャと同じです。ようやく目的の場所にたどり着きました。
先に進む前に、Goの構文が~を必要とする理由について議論しましょう。MySliceを常に許可したいと思うかもしれませんが、なぜそれがデフォルトではないのでしょうか?あるいは、厳密なマッチングをサポートする必要がある場合、なぜ[]Eの制約が名前付き型を許可し、例えば=[]Eの制約がスライス型リテラルのみを許可するように、物事を逆転させないのでしょうか?
これを説明するために、まず[T ~MySlice]のような型パラメータリストは意味をなさないことに注意しましょう。これは、MySliceが他の型の基底型ではないためです。例えば、type MySlice2 MySliceのような定義がある場合、MySlice2の基底型は[]stringであり、MySliceではありません。したがって、[T ~MySlice]はどの型も許可しないか、または[T MySlice]と同じであり、MySliceのみに一致します。いずれにしても、[T ~MySlice]は役に立ちません。この混乱を避けるために、言語は[T ~MySlice]を禁止しており、コンパイラは次のようなエラーを生成します。
invalid use of ~ (underlying type of MySlice is []string)
Goがチルダを必要とせず、[S []E]が基底型が[]Eである任意の型に一致する場合、[S MySlice]の意味を定義する必要がありました。
[S MySlice]を禁止することも、[S MySlice]がMySliceのみに一致すると言うこともできますが、どちらのアプローチも事前宣言された型で問題に直面します。intのような事前宣言された型は、それ自身の基底型です。基底型がintである任意の型引数を受け入れる制約を記述できるようにしたいと考えています。現在の言語では、[T ~int]と記述することでそれが可能です。チルダを必要としない場合でも、「基底型がintである任意の型」と表現する方法が必要になります。それを表現する自然な方法は[T int]でしょう。これは、[T MySlice]と[T int]が非常によく似ているにもかかわらず、異なる動作をすることを意味します。
おそらく、[S MySlice]は、その基底型がMySliceの基底型である任意の型に一致すると言うことができるかもしれませんが、それは[S MySlice]を不要で紛らわしいものにします。
~を要求し、型自体ではなく基底型に一致させる場合を明確にすることがより良いと考えています。
型推論
slices.Cloneのシグネチャを説明したところで、型推論によってslices.Cloneを実際に使用する方法がどのように簡素化されるかを見てみましょう。Cloneのシグネチャは次のとおりです。
func Clone[S ~[]E, E any](s S) S
slices.Cloneの呼び出しは、スライスをパラメータsに渡します。単純な型推論により、コンパイラは型パラメータSの型引数が、Cloneに渡されるスライスの型であると推論できます。その後、型推論は、Eの型引数がSに渡される型引数の要素型であることを認識するのに十分強力です。
これは、次のように記述できることを意味します。
c := Clone(ms)
と記述する必要はありません。
c := Clone[MySlice, string](ms)
Cloneを呼び出さずに参照する場合、コンパイラは推論に何も使用できないため、Sの型引数を指定する必要があります。幸いなことに、その場合、型推論はSの引数からEの型引数を推論でき、個別に指定する必要はありません。
つまり、次のように記述できます。
myClone := Clone[MySlice]
と記述する必要はありません。
myClone := Clone[MySlice, string]
型パラメータの分解
ここで使用した一般的なテクニック、つまり別の型パラメータEを使用して1つの型パラメータSを定義する方法は、ジェネリック関数シグネチャで型を分解する方法です。型を分解することにより、型のすべての側面を名前付けし、制約することができます。
例えば、maps.Cloneのシグネチャは次のとおりです。
func Clone[M ~map[K]V, K comparable, V any](m M) M
slices.Cloneと同様に、パラメータmの型に型パラメータを使用し、次に他の2つの型パラメータKとVを使用して型を分解します。
maps.Cloneでは、マップキー型に必要とされるように、Kを比較可能に制約します。コンポーネント型は、好きなように制約できます。
func WithStrings[S ~[]E, E interface { String() string }](s S) (S, []string)
これは、WithStringsの引数が、要素型にStringメソッドを持つスライス型でなければならないことを示しています。
すべてのGo型はコンポーネント型から構築できるため、型パラメータを使用してそれらの型を分解し、好きなように制約することができます。
次の記事: 型推論について知りたかったことすべて - そしてもう少し
前の記事: Go 1.22でのforループの修正
ブログインデックス