Goのジェネリクス先取り入門1

Yuji Kubota
Voicy Engineering
Published in
10 min readDec 9, 2020

この記事は Voicy Advent Calendar 2020 の 9日目の記事です。

昨日は @tamo_hory さんの「あなたが明日からKotlinを書くなら」です

Go11周年🎉

先月の11/10にGoが誕生してから11周年を迎えました!🎉
公式のブログでも11周年を祝う投稿がされています!
https://blog.golang.org/11years

待望のジェネリクス

ブログの中ではGoの今後についても一部触れられていますが、なんと言っても注目はジェネリクスです。

Generics

The next feature on everyone’s minds is of course generics. As we mentioned above, we published the latest design draft for generics back in June. Since then, we’ve continued to refine rough edges and have turned our attention to the details of implementing a production-ready version. We will be working on that throughout 2021, with a goal of having something for people to try out by the end of the year, perhaps a part of the Go 1.18 betas.

— — — — —

ジェネリクス

次に皆さんが気になっているのは、もちろんジェネリクスでしょう。上述したように、6月にジェネリクスの最新のデザインドラフトを公開しました。それ以来、私たちは大枠を改良し続け、リリースバージョンの詳細な実装に注力しています。私たちは2021年を通してこの作業に取り組み、年末までにはGo 1.18ベータ版の一部として、何かしら試していただくことを目標としています。

来年末とありますが、こちらのブログでは順調にいけば2021年8月に予定してるGo 1.17に追加される可能性もあるそうです。

また、6月に最新のデザインドラフトを公開したとありますが、11/25にさらに新しいドラフトが公開されています。この5ヶ月の間だけでも、型の定義方法がかなり変わっています。6月時点だと型パラメータの定義方法が

// 2020年6月時点
func Print(type T)(s []T) {
// same as above
}

のように (type T) と書いていたものが

// 2020年11月時点
func Print[T any](s []T) {
// same as above
}

[T any] に変わっています。ドラフトを読むと

To distinguish the type parameter list from the regular parameter list, the type parameter list uses square brackets rather than parentheses.

— — — — —
型パラメータリストと通常のパラメータリストを区別するために、型パラメータリストでは丸括弧ではなく角括弧を使用します。

とありました。たしかに6月時点の丸括弧が二つ並んでるのは見辛いのでこの修正はとてもよかったと思います。

そして、ジェネリクスを使うことができるプレイグランドも公開されています。リリース前のジェネリクスを手軽に試せるのはありがたいですね。
https://go2goplay.golang.org/

ジェネリクスを書いてみよう

それでは実際にジェネリクスを書いてみましょう。前述のように今はまだ5ヶ月で書き方がガラッと変わってしまう状況ですので、あくまで2020年12月時点のものとして読んでいただければと思います。

さきほどのプレイグランドを開くと、デフォルトで以下のようなコードが書いてあります。

func Print[T any](s []T) {
for _, str:= range s {
fmt.Print(str)
}
}
func main() {
Print([]string{"Hello, ", "playground\n"})
}

他の言語でジェネリクスを書いたことある人なら難しいことはないないかなと思います。ここでジェネリクス関数を呼び出している
Print([]string{・・・})
の部分ですが、これは実は型推論を使用した省略された書き方で、省略せずに書くと
Print[string]([]string{・・・})
となります。

anyとinterface{}

型パラメータの [T any] ですが、 any は他の言語でもよくあるどんな型でもOKと言う意味ですが、 any の他には何を指定できるのでしょう?試しに [T string] と書いてみると
string is not an interface
とコンパイルエラーで怒られてしまいました。ここに指定できるのはインターフェースだけのようです。

しかし、Goにはそもそもどんな型でもOKな interface{} というのがあります。それを使用すれば
func Print[T any](s []T) {・・・}
じゃなくても
func Print(s []interface{}) {・・・}
って書けばよくない?てかジェネリクスにインターフェースしか指定できないなら、インターフェースの型を直接引数に指定すればすればジェネリクス使わなくてもよくない?と思ったのですが、ドラフトにも似たようなことが書いてありました。

Writing a generic function is like using values of interface type: the generic code can only use the operations permitted by the constraint (or operations that are permitted for any type).

— — — — —

ジェネリクス関数を書くことは、インターフェース型の値を使うことに似ています。ジェネリクスコードは、制約によって許可された操作(または任意の型に対して許可された操作)のみを使うことができます。

ということでやっぱり似てるそうです。後半部分が指してることの具体的な意味がちょっとわからなかったのですが、、(インターフェース型を使用した場合でも、そのインターフェースで定義された関数(=許可された操作)しかよべないし。うーん)わかる方いたらコメントください。

ただ、以下のような場合、 s1s2 は同じ型でないといけなくなりますが、これを interface{} で書くとそれぞれ違う型も指定できてしまいます。

func Print[T any](s1 []T, s2 []T) { ... }

また、この例のPrint関数を呼び出す場合で考えたらジェネリクスは便利だと思います。例えば
func Print[T any](s []T) {・・・}
を呼び出すときは

vals := []string{"hoge", "fuga"}
Print(vals)

のように、事前に定義した配列をそのまま指定して呼べますが、
func Print(s []interface{}) {・・・}
の場合だと

vals := []string{"hoge", "fuga"}
args := []interface{}{}
for _, val := range vals {
args = append(args, val)
}
Print(args)

のように、一度 interface{} の配列に入れ替えないと呼べないので。これは結構めんどくさいなと思いながら書いてたことがあるので助かります。

ちなみに
func Print[T interface{}](s []T) {・・・}
と書いても、anyと同じように呼ぶことができます。

じゃあ anyinterface{} って何が違うの?となりますが、

However, it‘s tedious to have to write interface{} every time you write a generic function that doesn’t impose constraints on its type parameters. So in this design we suggest a type constraint any that is equivalent to interface{}.

— — — — —

しかし、型を指定しないパラメータのジェネリクス関数を書くたびにinterface{}を書くのは面倒です。そこで、私たちはこの設計でinterface{}と同等の型anyを提案しています。

とあり、完全に同じ意味のようです。そうなると変数を定義する際も、
var hoge interface{}
の代わりに
var hoge any
と書けるのかと思ったのですが、残念ながらコンパイルエラーになりました。その理由としては

(Note: clearly we could make any generally available as an alias for interface{}, or as a new defined type defined as interface{}. However, we don’t want this design draft, which is about generics, to lead to a possibly significant change to non-generic code. Adding any as a general purpose name for interface{} can and should be discussed separately).

— — — — —

明らかに、anyをinterface{}のエイリアスとして、あるいはinterface{}として定義された新しい型として、一般的に利用できるようにすることが可能です 。しかし、このドラフト設計はジェネリクスに関するものであり、ジェネリクスではないコードに重大な変更をもたらす可能性があることを望んでいません。interface{}の汎用的な名前としてanyを追加することは、別途議論することができますし、議論すべきです)。

ということで、あらゆる場所で interface{} の代わりにで any を使用可能にすることは可能かもしれないが、ジェネリクスとは別の設計として議論すべきことなので、今回はジェネリクスの型パラメータでのみ使用可能にしているらしいです。 interface{} は打つのが面倒なので早く any 使えるようになって欲しいですね。

ジェネリクス変数

ここまではジェネリクスを使用した関数の定義について見てきましたが、つぎは変数型の定義について見てみましょう。

type Vector[T any] []T

これは任意の型の配列を定義しており、このインスタンスを作成する際は以下の通りです。

var v Vector[int]
または
v := Vector[int]{}

この型にメソッドを定義したい場合は

func (v *Vector[T]) Push(x T) {
*v = append(*v, x)
}

となります。メソット定義の時の型パラメータ名は同じである必要はなく、

func (v *Vector[X]) Push(x T) {・・・}

でも大丈夫です。まぁ特に理由がなければ同じ名前にしておいた方がみやすいと思いますが。また、

In particular, if they are not used by the method, they can be _.

— — — — —
特に、それらがメソッドで使用されない場合は、_とすることができます。

とあるので、型パラメータがメソッドないの処理で必要なければ

func (v *Vector[_]) Push() {・・・}

と書けるのかなと思いましたが、プレイグランドで試したらコンパイルエラーになりました。これはプレイグランドがまだ未対応なだけの可能性もあるかと思います。

Coming Soon…

書いていたら思っていたよりもボリュームがあったので、2回に分けて書くことにしました。ということで、続きは12月14日を予定してますので、お楽しみに!

2020/12/14 追記
続き公開しました!

--

--