はじめに
まもなくリリース予定(2022年2月)のGo 1.18では、待望?のGenericsが導入されます。そこで、現在利用できるGo 1.18beta1で、Genericsの使い方を紹介します。
GoのGenerics
まず始めに、現時点でのGenericsの仕様や制限について紹介します。
Genericsの主な仕様
Genericsの主な仕様は以下のとおりです。
- 関数や型宣言の構文に型パラメータが使えるようになった
- パラメータ化された関数や型は、その後に[]で型引数のリストを記述することでインスタンス化することができる
- インタフェース型の構文では、任意の型、union、T型要素を埋め込む事ができるようになった
- any識別子(空のインタフェースのエイリアス)が追加された
- comparable識別子(==や!=を使って比較できる全ての型の集合を表すインタフェース)が追加された
より詳しい言語仕様はproposalをご覧ください。
Genericsの制限
現在のGenericsの実装には以下の制限があります。
- GoのコンパイラはGenerics関数やメソッドの内部での型宣言ができない(Go 1.19でサポートされる予定)
- Goのコンパイラは予め宣言されたreal関数、imag関数、complex関数でのパラメータ型の引数を受け入れない(Go 1.19でサポートされる予定)
- 型パラメータや型パラメータへのポインタを構造体の無名フィールドとして埋め込む事はできない。また、インタフェース型に型パラメータを埋め込む事もできない
- 2つ以上の項を持つunion要素は、空でないメソッドセットをもつインタフェース型を含むことができない
Genericsを使ってみる
基本形
では、実際にGenericsを使ったメソッドを書いてみます。比較のために、まずはGenericsを使わない場合のメソッドを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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つの関数をまとめる事ができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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型
の値を持つマップを引数として渡した場合、コンパイルエラーになります。
1 | cannot use intFruits (variable of type map[string]int32) as type map[string]int64 in argument to SumFruits[string, int64] |
注意点として、型パラメータはGenericsコードがそのパラメータに対して実行する全ての操作をサポートしなければなりません。例えば、型パラメータに数値型が含まれているGenerics関数で文字列操作を実行しようとすると、コードはコンパイルされません。
呼び出しの際の型引数を省略する
多くの場合、呼び出し元のコードから型を推論することができるため、呼び出し時に型引数を省略することができます。先程のコードを修正すると、以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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
のように冗長な記述になっています。そこで、これらを集約した型を定義することで、様々なところで利用できるようになるので、制約がより複雑になった場合などに、コードの効率化に役立ちます。
修正したコードは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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の力を実感するのが楽しみです。