はじめに
2022年3月なので、既に数ヶ月以上前の話にはなりますが…
Go のver1.18がリリースされ、新機能としてジェネリクスが使えるようになりました。
現在私が仕事で触っているのはver1.16なのでジェネリクスの恩恵を受けていないのですが
どうやらものすごく便利な機能らしいので少し触ってみようかなと思います。
編集履歴
2022.08.02
・ver1.18以前、interface{}の弊害としてのpanicに関する記述を追加
・「ジェネリクスが追加されて個人的に嬉しくなったこと(補足)」を追加
・参考リンク追加
ジェネリクスとは??
ジェネリック(総称あるいは汎用)プログラミング(英: generic programming)は、具体的なデータ型に直接依存しない、抽象的かつ汎用的なコード記述を可能にするコンピュータプログラミング手法である。 wikipediaより
ちょっと何言ってるか分からない。
静的型付け言語ではお馴染みの機能らしいが、ver1.18以前のGoにはなかった機能。
超ざっくり言うと、関数とかで汎用的に(generic)型を受け付けるような形で定義できるみたい。
動的型付け言語ばっかり触ってたからよく分からない
ver1.18以前
リリースノートでの例を使って触ってみる。環境は、前回使ったDocker環境がたまたま1.18だったので使い回し。
package main
import "fmt"
func Min(x, y float64) float64 {
if x < y {
return x
}
return y
}
func main() {
fmt.Println(Min(3, 2)) // 2
}
2つの値の小さい方を出力するだけのとてもシンプルなMin関数
を出力するだけのもの。
ですが、2つ受け取る引数はどちらも、float64じゃないと受け取ってくれません。
func main() {
var x int = 3
var y int = 2
fmt.Println(Min(x, y))
}
なんてしてみると
./main.go:15:18: cannot use x (variable of type int) as type float64 in argument to Min
./main.go:15:21: cannot use y (variable of type int) as type float64 in argument to Min
はぁ!?int型なんて受け取れないんですけど??と怒られます。
ver1.18以前は、真っ当に書こうとすると、引数及び戻り値ごとに関数を作ってあげるのが正着。
func Float64Min(x, y float64) float64 {
if x < y {
return x
}
return y
}
func IntMin(x, y int) int {
if x < y {
return x
}
return y
}
あるいは、interface{}型
という何でもアリな型があったので例えば
func Min(x, y interface{}) float64 {
switch x.(type) {
case int:
switch y.(type) {
case int:
if x.(int) < y.(int) {
return float64(x.(int))
}
return float64(y.(int))
}
case float64:
switch y.(type) {
case float64:
if x.(float64) < y.(float64) {
return x.(float64)
}
return y.(float64)
}
}
return 0
}
とかやれば、超無理やりどうにかすることができた。
適当に書いたのでもうちょっとマシな書き方できたと思う。
ただ、x.(floay64)
のように型キャストだらけで気持ち悪い。
この程度の関数ならまだしも、たとえばlistとかmapとかで中身の型がなんであれ同じような挙動をさせたい関数だと、引数の型ごとに関数を作るのはとてもめんどくさいです。
そして、interface{}
で逃げるともう1つ困ることがあります。それは、panicです。
func Int64PlusOne(x interface{}) int64 {
return x.(int64) + 1
}
func main() {
var name1 string = "伊達朱里紗"
fmt.Println(Int64PlusOne(name1))
// panic: interface conversion: interface {} is string, not int64
明らかにint64
しか受け付けなさそうなInt64PlusOne
関数の仮引数をinterface{}
にしてみたことで
string型
のname1
を実引数にInt64PlusOne
を実行しようとしても、コンパイルでは検知できません。
実行した後にpanicを起こしてプログラムが停止します。予期せぬ不具合に繋がりそうで嫌ですね。
ちなみに私はpanicが本当に嫌いです。(私がパニックになるから)
そこで登場したのがジェネリクス。リリースノートの記述からして、Golang最大級のアップグレードらしい。
ver1.18から使用できる記法
func GenericMin[T constraints.Ordered](x, y T) T {
if x < y {
return x
}
return y
}
func main() {
var a float64 = 100.88
var b float64 = 200.7
fmt.Println(GenericMin(a, b)) // 100.88
var x int = 3
var y int = 2
fmt.Println(GenericMin(x, y)) // 2
var name1 string = "伊達朱里紗"
var name2 string = "岡田紗佳"
fmt.Println(GenericMin(name1, name2)) // 伊達朱里紗(バイト単位で「伊」の方が「岡」より小さい)
}
GenericMin
の方では、先程のように仮引数にfloat64
もinterface{}
も置いていませんが
main.go
で呼び出す側の実引数で型を明示して渡してあげることで、int
もfloat64
も、何ならstring
でもGenericMin
を使うことができました。
具体的に何が変わったのか
GenericMin[T constraints.Ordered]
の[]
のような記法は、1.18以前には存在しませんでした。
これは型パラメータというもので、[]
から推測できる通り、宣言した関数で使用できる型を「型パラメータリスト」において定義するというもの。
関数宣言の段階で、使用できる型に制約を与えてあげることで、複数の型で使用できる関数を定義できる、ということのようです。
上記の例では、Goのサブリポジトリからconstraints.Ordered
(大小比較できる、intとかfloatとかそういうやつ)をGenericMin関数
の型パラメータとして、関数宣言を行っています。
これにより、上述のpanicの問題も解消されます。
GenericMin
を呼び出している側で宣言された型が有効なものかどうか、という判定をコンパイルで済ませてしまうからです。
panicざまぁ
fmt.Println(GenericMin[float64](64.22, 34.89)) // 34.89
という風に、GenericMin
を呼び出す時に[]
を使って型を明示することもできます。
このように、ジェネリックに作った関数に型引数を与えることをインスタンス化といいます。
前述の通り型パラメータはリストなので、こんな感じにすることもできます。
// float64 か int を受けつける型パラメータに変更
func GenericMinExceptStr[T float64 | int ](x, y T) T {
if x < y {
return x
}
return y
}
func main() {
var a float64 = 100.88
var b float64 = 200.7
fmt.Println(GenericMinExceptStr(a, b)) // 100.88
var x int = 3
var y int = 2
fmt.Println(GenericMinExceptStr(x, y)) // 2
fmt.Println(GenericMinExceptStr[float64](64.22, 34.89)) // 34.89
var name1 string = "伊達朱里紗"
var name2 string = "岡田紗佳"
fmt.Println(GenericMinExceptStr(name1, name2))
// string型は受け付けてないので、「コンパイルの段階で」エラーになる
}
ジェネリクスが追加されて個人的に嬉しくなったこと(補足)
最近(2022.07.22)になり、mapやslice周りの関数が追加されました。
たとえば、jsとかではごく当たり前に使われているObject.keys(obj)
みたいな連想配列からキーをまとめて取得するみたいなメソッド。
ver1.18以前のGoにはありませんでした。
forループ使ったりして自作するしかなかったんですね。下記コードは他所から丸々もらってきました。(参考リンク)
package main
import (
"fmt"
)
func main() {
persons := map[string]int{"伊達": 30, "岡田": 29, "瑞原": 35, "二階堂": 40}
// personsの全部のkeyを取得する処理
var keys []string
for key := range persons {
keys = append(keys, key)
}
fmt.Println(keys) // [伊達 岡田 瑞原 二階堂]
}
Goではmapでもsliceでも宣言時に内容物の型定義が必要なので、それらすべてに対応する形でpackage側で関数作れなかった(作るのがめんどくさかった)から無かったんだろうと思いますが。
ところが、Goのサブリポジトリであるgolang/x/exp
パッケージが更新され、maps
やslices
といったものが追加、他言語でお世話になりがちな関数の一部が使用できるようになりました。
package main
import (
"fmt"
"golang/x/exp/maps"
)
func main() {
persons := map[string]int{"伊達": 30, "岡田": 29, "瑞原": 35, "二階堂": 40}
fmt.Println(maps.Keys(persons)) // [伊達 岡田 瑞原 二階堂]
}
はい、神アプデ。
せっかくなので、maps.Keys()
の定義を覗いてみましょう。
func Keys[M ~map[K]V, K comparable, V any](m M) []K
思いっ切りジェネリクスの恩恵受けまくりな関数定義です。素敵です。
~map[K]V
の~
は、Underlying Type
というものを示し、基礎となる型がmap[K]V
であることを要求するもの、という意味合いとのことです。(詳細は参考リンクへ)
K comparable
で出てくるcomparable型
は、型パラメータでのみ使用できるインターフェースで、文字列・数値・真偽値・ポインタなど比較可能な連中の総称(?)のようなイメージです。
V any
のany
は、ver1.18以前のinterface{}
です。というか、ver1.18でしれっとinterface{}
のエイリアスとしてany
が追加されました。interface{}って、{}があって気持ち悪かったもんね。
ジェネリクスが登場したことで、これまで他言語では簡単だったのにGoでは難しかったことが簡単に出来るようになるパッケージが実装されたり、パッケージ周りの盛り上がりも期待できるかもしれません。
最後に
Go自身が最大級のアップグレードと言うだけのことがある、かなり使い勝手の良さそうな変更点という印象を受けました。
特にpanic大嫌いな私としては、これまでinterface{}に頼ってpanicを起こしていた部分をコンパイル時点で判別して修正できるってのは最高に気持ちよさそうだなと思います。
あとは、少し紹介したmapsやslicesがもっと充実すると実装が楽になるんだけどなぁなんて思ったり。
参考リンク
Go公式 ジェネリクスの紹介
https://go.dev/blog/intro-generics
当記事の内容について、100倍くらい深く解説されている記事
zenn Nobishii様
https://zenn.dev/nobishii/articles/type_param_intro
https://zenn.dev/nobishii/articles/type_param_intro_2
ver1.18以前の方法でmapからキーの一覧の取り出し方を紹介されている記事
https://asapoon.com/golang/5399/how-to-get-map-keys/
型パラメータのチルダについての解説
zenn waawaa様
https://zenn.dev/waawaa/articles/ac07cf53a69cccca156c