まもなくリリース予定(2022年2月)のGo 1.18では、待望?のGenericsが導入されます。そこで、現在利用できるGo 1.18beta1で、Genericsの使い方を紹介します。
まず始めに、現時点でのGenericsの仕様や制限について紹介します。
Genericsの主な仕様は以下のとおりです。
より詳しい言語仕様はproposalをご覧ください。
現在のGenericsの実装には以下の制限があります。
では、実際にGenericsを使ったメソッドを書いてみます。比較のために、まずはGenericsを使わない場合のメソッドを定義します。
package main import "fmt" func main() { intFruits := map[string]int64{ "apple": 10, "orange": 8, } floatFruits := map[string]float64{ "first": 11.0, "second": 8.8, } fmt.Printf("Sums: %v and %v\n", SumIntFruits(intFruits), SumFloatFruits(floatFruits)) } func SumIntFruits(m map[string]int64) int64 { var s int64 for _, v := range m { s += v } return s } func SumFloatFruits(m map[string]float64) float64 { var s float64 for _, v := range m { s += v } return s }
int64型
のマップとfloat64型
のマップの合計値を返す2つの関数が定義されています。Genericsを使うと、これら2つの関数をまとめる事ができます。
package main import "fmt" func main() { intFruits := map[string]int64{ "apple": 10, "orange": 8, } floatFruits := map[string]float64{ "first": 11.0, "second": 8.8, } fmt.Printf("Sums: %v and %v\n", SumFruits[string, int64](intFruits), SumFruits[string, float64](floatFruits)) } func SumFruits[K comparable, V int64 | float64](m map[K]V) V { var s V for _, v := range m { s += v } return s }
SumIntFruits関数
とSumFloatFruits関数
のをまとめて、SumFruits関数
を定義しました。この関数では、comparable
を満たす型のキーを持ち、int64型
もしくはfloat64型
の値を持つマップを引数として受けとることができます。
型パラメータ(SumFruits関数のKやV)の制約は通常、型のセット(int64 | float64のように)を表しますが、コンパイル時には単一の型(呼び出し元のコードで渡している値の型)を表します。
なお、型パラメータとして許容していない、int32型
の値を持つマップを引数として渡した場合、コンパイルエラーになります。
cannot use intFruits (variable of type map[string]int32) as type map[string]int64 in argument to SumFruits[string, int64]
注意点として、型パラメータはGenericsコードがそのパラメータに対して実行する全ての操作をサポートしなければなりません。例えば、型パラメータに数値型が含まれているGenerics関数で文字列操作を実行しようとすると、コードはコンパイルされません。
多くの場合、呼び出し元のコードから型を推論することができるため、呼び出し時に型引数を省略することができます。先程のコードを修正すると、以下のようになります。
package main import "fmt" func main() { intFruits := map[string]int64{ "apple": 10, "orange": 8, } floatFruits := map[string]float64{ "first": 11.0, "second": 8.8, } fmt.Printf("Sums: %v and %v\n", SumFruits(intFruits), SumFruits(floatFruits)) } func SumFruits[K comparable, V int64 | float64](m map[K]V) V { var s V for _, v := range m { s += v } return s }
このように、SumFruits関数
の呼び出し時に型引数を省略することができます。
先程のコードでは、V int64 | float64
のように冗長な記述になっています。そこで、これらを集約した型を定義することで、様々なところで利用できるようになるので、制約がより複雑になった場合などに、コードの効率化に役立ちます。
修正したコードは以下の通りです。
package main import "fmt" type Number interface { int64 | float64 } func main() { intFruits := map[string]int64{ "apple": 10, "orange": 8, } floatFruits := map[string]float64{ "first": 11.0, "second": 8.8, } fmt.Printf("Sums: %v and %v\n", SumFruits(intFruits), SumFruits(floatFruits)) } func SumFruits[K comparable, V Number](m map[K]V) V { var s V for _, v := range m { s += v } return s }
int64
とfloat64
のunion
をNumber
と定義することで、よりコードがシンプルになります。
Go 1.18で導入されるGenericsについて紹介しました。Generics自体は何年も前から議論されてしましたが、ついに導入されることとなり、実アプリケーションでGenericsの力を実感するのが楽しみです。