10分de技術知 golangのGenerics - slicesパッケージの関数定義がわかるようになるぐらいの技術知まとめ
golangのGenerics
概要
GolangのGenerics機能について10分で読めるくらいのまとめ。
本記事の目的は2つあります。
1つは「標準ライブラリslicesパッケージの関数定義がわかるようになるぐらいの技術知まとめ」ぐらいの、Genericsに関する知識を得ること(・・・というか、著者はそのために勉強した)。
例えば、slicesパッケージのContains関数の定義が下記です。初見だと意味が分からず。元々、私はJavaのGenericsやC++のテンプレートに関する知識はあるので、勉強しなくてもなんとなくわかるだろ、ぐらいに思っていましたが・・・(特に、comparableやチルダ~
の意味がよくわからない)。
func Contains[S ~[]E, E comparable](s S, v E) bool
2つ目はGoのGenericsに関連する用語の意味を理解すること。type parameterやtype constraintなどなど。
Genericsとは
Genericsは、Go1.18から導入された新しい言語仕様。
関数や構造体に型パラメータ(type parameter)を定義することができる。関数や構造体の利用者は、型パラメータに対して具体的な型を指定し、その関数や構造体を利用する。
Genericsのメリットは、関数や構造体を、複数の型で利用できること。
具体例
下記の2つの関数(AddNumberInt、AddNumberFloat32)は中身は同じ。引数、返り値の型だけが異なる。
package main
import "fmt"
func main() {
fmt.Println(AddNumberInt(1, 2), AddNumberFloat32(1.0, 2.0))
}
func AddNumberInt(a, b int) int {
return a + b
}
func AddNumberFloat32(a, b float32) float32 {
return a + b
}
上記2つの関数を、1つの関数にしてみる。すると下記のようになる。
package main
import "fmt"
func main() {
fmt.Println(
AddNumber[int](1, 2),
AddNumber[float32](float32(1.0), float32(2.0)),
)
}
func AddNumber[N int | float32](a, b N) N {
return a + b
}
型パラメータは[N int | float32]
。AddNumber関数は複数の型、intとfloat32で利用できる。
2つあった関数を1つにまとめることができた。冗長なソースコードが減った。これがGenericsのメリット。
Genericsの基本
Genericsの記法
型パラメータ(type parameter)
型パラメータ(type parameter)は、関数(function)と型(type)に対して付与できます。型パラメータは、カンマ区切りで複数指定することができます。
型引数(type argument) と 型制約 (type constraint)
型パラメータは、型引数(type argument)と型制約(type constraint)で構成されます。
関数
func 関数名[型引数 型制約,...] (引数) 返り値 {
関数の中身
}
// 例
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
// `[K comparable, V int64 | float64]` カンマ区切りで複数の型パラメータがある例。
// `K comparable`が1つ目の型パラメータ
// `K`が型引数、`comparable`が型制約
// `V int64 | float64`が2つ目の型パラメータ
// `V`が型引数、`int64 | float64`が型制約
構造体
// 構造体
type 構造体名[型引数 型制約,...] struct {}
// メソッド
func (t 構造体名[型引数]) メソッド名(引数) 返り値 {
メソッドの中身
}
// 例
type Computer[K comparable, V int64 | float64] struct {
m map[K]V
}
func (t *Computer[K, V]) Set(k K, v V) {
t.m[k] = v
}
func (t *Computer[K, V]) Sum() V {
var s V
for _, v := range t.m {
s += v
}
return s
}
インターフェース
// インターフェース
type インターフェース名[型引数 型制約,...] interface {
}
// 例
type ComputerInterface[K comparable, V int64 | float64] interface {
Set(k K, v V)
Sum() V
}
型定義
こんなこともできる。しかしどんな意味があるのだろうか?
// 例
type A[V comparable] int64
type B A[string]
型制約
型制約は、関数の利用側がその型パラメータにおいて利用可能な型。
例えば、下記の関数の場合
func FuncA[K comparable, V int64 | float64](a1 K, a2 V)
型パラメータ K comparable
において、comparable
が型制約。関数の利用側は、2つの演算子==
と!=
で比較できる型を、Kに指定できる。
型パラメータ V int64 | float64
において、int64 | float64
が型制約。関数の利用側は、int64またはfloat64のどちらかの型を、Vに指定できる。
関数の利用側の記述例。
func main5() {
FuncA(1, int64(1))
FuncA("hoge", float64(1))
// Kに指定された構造体も==と!=で比較的るため、comparableな型として指定可能
FuncA(struct{}{}, float64(1))
// 下記はNG Vにはfloat32を指定できない
// FuncA(1, float32(1))
// 下記はNG Kに指定されたsliceやmapはcomparableな型ではない
// FuncA([]int64{1, 2, 3}, float64(1))
// FuncA(map[string]string{"hoge": "fuga"}, float64(1))
}
チルダ(~)が付いた型制約
型制約にチルダがついていることがある。チルダの意味は下記。
- 型制約
~T
は、関数の利用側は、Tを基礎型(underlying type)とする全ての型を、型パラメータとして指定できる。
例えば、下記の関数。
func f2[N ~int64](n N) {}
f2の利用側は、int64を基礎型とする全ての型を、型パラメータとして指定できる。
ゆえに、f2の利用側の記述例は下記のようになる。
package main
func main() {
f1(int64(1))
f2(int64(1))
f1(X(1)) // コンパイルエラーとなる
f2(X(1))
}
// Xという型を宣言する。
// この時、X の基礎型は int64 となる。
type X int64
// f1 は、int64 型のみを許容する。
// f1 は、int64を基礎型として持つ X を許容しない。
func f1[N int64](n N) {}
// f2 は、int64 型を基礎型として持つ全ての型を許容する。
// よって、X を許容する。
func f2[N ~int64](n N) {}
Goの言語仕様として、1つの型は基礎型を1つの基礎型を持つ、というものがある。
その型の基礎型が何か?は言語仕様に書かれているが、詳細はかなり長くなりそうなので、ここでの説明は割愛する。知りたい方は言語仕様を。
型制約(type constraint)の宣言
型制約をinterfaceとして宣言できる。宣言された型制約は、複数箇所で利用されることができる。
// 型制約 MyNumber の宣言
type MyNumber interface {
int | float32
}
// 宣言された型制約 MyNumber を利用する
func AddNumber[N MyNumber](a, b N) N {
return a + b
}
型指定の省略
型パラメータが付与された関数の利用側のコードでは、その関数の型パラメータが推論可能な場合、型の指定を省略することができる。
package main
import "fmt"
func main() {
// 型パラメータが付与された関数AddNumberの利用側のコード
fmt.Println(
// AddNumber[int](1, 2),
// AddNumber[float32](float32(1.0), float32(2.0)),
AddNumber(1, 2), // [int]を省略可
AddNumber(float32(1.0), float32(2.0)), // [float32]を省略可
)
}
// AddNumber は型パラメータが付与された関数
func AddNumber[N int | float32](a, b N) N {
return a + b
}
ビルトインな型制約 comparable
comparable
は「2つの演算子==
と!=
で比較できる型」という意味。
これはビルトインな型制約であり、言語仕様に記載されている。
あらゆるプリミティブ型(intやfloatや文字列型、etc...)はcomparable。構造体もcomparableである。
comparableとは何か?の説明は、これまた長くなりそうなので割愛する。多分この辺に書いてある。
slicesパッケージの型パラメータの意味
ここまで説明した知識があれば、標準ライブラリ slices パッケージの意味を理解できる(・・・と思います。はい汗)。
genericsもう怖くない。
cmpパッケージ
型制約 Ordered は下記のように宣言されている。この宣言の意味は、「Orderedは< <= >= >を適用可能な型」ということ。日本語で要約すると「順序がある型」という意味。
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
型制約Orderedは、チルダ~
が用いられているため、基礎型から派生する全ての型を許容する。チルダ~
によって、より多くの型で利用可能な型制約を実現できるんですなぁ。
// 順序がある2つの変数を比較する関数
func Compare[T Ordered](x, y T) int
slicesパッケージ
func Contains[S ~[]E, E comparable](s S, v E) bool
この関数は、配列 s の中に要素 v が含まれるかどうかを判定する関数。
Eはcomparableな型。
Sは[]Eを基礎型に持つ型。
comparableと基礎型について、もう少し言及したいかも。