Goブログ
型パラメーターの分解
slicesパッケージの関数シグネチャ
slices.Clone
関数は非常にシンプルです。任意の型のスライスのコピーを作成します。
func Clone[S ~[]E, E any](s S) S {
return append(s[:0:0], s...)
}
これは、ゼロ容量のスライスに追加すると、新しいバッキング配列が割り当てられるために機能します。関数本体は関数シグネチャよりも短くなります。これは、本体が短いという理由もありますが、シグネチャが長いという理由もあります。このブログ記事では、シグネチャがなぜこのように書かれているのかを説明します。
シンプルなClone
まず、シンプルなジェネリックな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
はMySlice
型ではなく、[]string
型の値を返します。型[]string
にはString
メソッドがないため、コンパイラーはエラーを報告します。
柔軟なClone
この問題を解決するには、引数と同じ型を返す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
の基になる型はMySlice
ではなく、[]string
です。したがって、[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]
型パラメーターの分解
ここで使用した一般的な手法(1つの型パラメーターE
を使用して別の型パラメーター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ループの修正
ブログインデックス