見出し画像

Generics in Go

よろしくどうぞ、@knwoop です。

記事のテーマは Go 1.18 でリリースされる Generics についての解説です。

使用する環境・バージョン

OS: macOS Big Sur 11.5.1 go version go1.18beta1 darwin/amd64

⚠️ Generics はまだ正式にリリースしてはいません。リリースまでに、多少機能に変更が入る可能性があります。

準備

まず、Generics はまだリリースされていませんので、まずローカル環境で使えるようにしましょう。

beta コマンドを使えるようにします

$ go install golang.org/dl/go1.18beta1@latest
$ go1.18beta1 download
$ go1.18beta1 version

go1.18beta1 を go のエイリアスにします

$ alias go=go1.18beta1
$ go version
go version go1.18beta1 darwin/amd64

参考: go.dev/doc/tutorial/generics

これまでの苦悩

私たち Gopher は Contains と幾度となく戦ってきました。事ある毎に slice の中身を見て、特定の要素の存在チェックする関数を書き続けてきました。それも型毎に...

まず、int 型の slice に 特定の値が含まれているか判定する関数を作ってみます。

func Contains(needle int, haystack []int) bool {
	for _, v := range haystack {
		if v == needle {
			return true
		}
	}
	return false
}

go.dev/play/p/E5dhKNM1Tii

そこで次に int64 を型に受け取る Contains 関数を作成する必要がでてきました。

// 関数名を Contains から ContainsInt に変更
func ContainsInt(needle int, haystack []int) bool {
	for _, v := range haystack {
		if v == needle {
			return true
		}
	}
	return false
}

// 新たに追加
func ContainsInt64(needle int64, haystack []int64) bool {
	for _, v := range haystack {
		if v == needle {
			return true
		}
	}
	return false
}

go.dev/play/p/awCUCoQJ8qB

そして次は、int32、それから uint64 でも...

Go を長い間使ってきましたが、 上記のようなケースで Contains 関数を何度も書いてきました。(筆者は Contains 関数用の code snippet を作成しているほどです...)

そしてついに、Generics が accepted されました。

Generics とはなんなのか?

一言でいえば、いろんな型に対して汎用的な機能などを書くための目的で導入されたものです。つまり、Generics を使うことで、その場で型を指定せずに、データの構造や関数を記述できます。

そして、Go における Generics のキーワードは3つあります。

  • type parameters (型パラメータ)

  • type sets (型セット)

  • constrains package

Type parameters

まず、最初の関数に “Type Parameters” を入れてみます。Generics を使うと "Type Parameters" という新しいパラメータを関数で使用できます。

func Contains[T any](needle T, haystack []T) bool {
	for _, v := range haystack {
		if v == needle {
			return true
		}
	}
	return false
}

上記のコードだと [T any] が Type Parameters 部分になります。そして、引数である needle と haystack が Type Parameters の型で決定されるというのが直感的にわかります。しかし、このままだとコンパイルエラーになります。 実は、== で比較するときは any ではなくて comparable を使う必要があります。最終的なコードはこのようになります。

func Contains[T comparable](needle T, haystack []T) bool {
	for _, v := range haystack {
		if v == needle {
			return true
		}
	}
	return false
}

gotipplay.golang.org/p/f2Bo6EFVWmt

⚠️ any と comparable はビルトインになります。comparable は大小比較ではない。 comparable については、こちらが参考になりそうです。 -> go.dev/ref/spec#Comparison_operators

どうでしょうか。これで無事、Contains 地獄から脱出できました!

Type sets

ユースケースによったら、もっと型を絞りたい場合などがあります。先程作成した Contains 関数が受け取れる値を符号なし( unsigned )整数のみにしたい場合を考えてみます。

そこで便利なものが Type sets です。これは、型の集合に名前をつけられるようになるというものです。

Type sets を使って符号なし整数の型の集合を作成するとこのようになります。

type PredeclaredUnsignedInteger interface {
	uint | uint8 | uint16 | uint32 | uint64 | uintptr
}

この仕様は、spec: generics: use type sets to remove type keyword in constraints の issue で accepted されたものです。内容自体すごく面白いのでおすすめです。

このように型の集合を interface を使って表せます。 では、Type sets を使った Contains 関数を実装してみると以下のようになります。

package main

import (
	"fmt"
)

type PredeclaredUnsignedInteger interface {
	uint | uint8 | uint16 | uint32 | uint64 | uintptr
}

func Contains[T PredeclaredUnsignedInteger](needle T, haystack []T) bool {
	for _, v := range haystack {
		if v == needle {
			return true
		}
	}
	return false
}

呼び出し先では、特に変わらず実行できます。

s := []uint{1, 2, 3, 5, 8, 13}
fmt.Println(Contains(3, s)) // true
fmt.Println(Contains(7, s)) // false
fmt.Println(Contains(13, s)) // true

gotipplay.golang.org/p/GXuos7lVX75

試しに負の整数を渡してみるとエラーになります。

fmt.Println(Contains(-7, s))
// cannot use -13 (untyped int constant) as uint value in argument to Contains (overflows)

gotipplay.golang.org/p/JS1hz70ceKQ

期待通り「符号なし( unsigned )整数のみ」を渡せるようできましたね。

ただし、これには少し問題があります。 スライスの要素に独自の型を定義して渡したい場合などよくあることです。

type MyUint uint

s := []MyUint{1, 2, 3, 5, 8, 13}
fmt.Println(Contains(3, s))

ここでは、独自の型に MyUint を定義して、そのスライスを Contains 関数に渡そうとしましたが、これはエラーになりました。単に型が違うことによるエラーです。PredeclaredUnsignedInteger interface に MyUint を追加することで問題は解決しますが、その場合独自の型を定義するたびに interface を変更しなければなりません。

approximation element (近似要素)

approximation element を使うことによって underlying type (基礎となる型)をマッチさせるようにできます。型の前にチルダを置くことで使用できます。

PredeclaredUnsignedInteger というインターフェース名に違和感を感じますが、これは approximation element を使った interface 型と区別するためです

それではやってみましょう。新しく UnsignedInteger という名前で Type sets を作成してみます。

type UnsignedInteger interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

それを Contains 関数に組み込んでみます。

func Contains[T UnsignedInteger](needle T, haystack []T) bool {
	for _, v := range haystack {
		if v == needle {
			return true
		}
	}
	return false
}

すると 同じ underlying type を持っているカスタム定義した型でも渡すことができます。

s := []MyUint{1, 2, 3, 5, 8, 13}
fmt.Println(Contains(3, s))

gotipplay.golang.org/p/H1Yi8lId9o4

Constrains package

ここまで見てきて、Go の Type sets が便利だというのを理解できました。しかし、ここで型の列挙が大変だと思いませんか?それに、符号なし整数の型の列挙も漏れの可能性も十分にありえます。

そこで便利なのが Constrains package です。パッケージの中身を見ていただけるとわかりますが、先程使ったような型の集合がすでに定義されています。Go 1.18 からさっそく使えるます。

これまで書いたコードにこれを当てはめると、以下のようになります。 すごく完結になりました。

package main

import (
	"constraints"
	"fmt"
)

func Contains[T constraints.Unsigned](needle T, haystack []T) bool {
	for _, v := range haystack {
		if v == needle {
			return true
		}
	}
	return false
}

type MyUint uint

func main() {
	s := []MyUint{1, 2, 3, 5, 8, 13}
	fmt.Println(Contains(3, s))
	fmt.Println(Contains(7, s))
	fmt.Println(Contains(13, s))
}

gotipplay.golang.org/p/_nylUaItIgT

Generics を使う前に...

Go における Generics の便利さすばらしさを体感できたのではないでしょうか?

筆者としては、Generics を使う前にどうしても伝えたい言葉があります。

"write code, don't design types"

この言葉は、Go Day on Google Open SourceUsing Generics in Go のセッションで使われた言葉です。

まず設計から考えるのではなくコードを書くところから始めよということです。筆者は Go を 5年ほど使ってきましたが Generics が欲しいと思ったことはほとんどありませんでした。たしかに、記事の内容のような面倒くさいと感じるところはあるのですが、それが Go っぽくて好きなところでもありました。 そのセッションでも言っていることではありますが、 Generics を使ったからと言って処理速度が上がるわけでもありません。

HashiCorp, Inc. の Mitchell Hashimoto 氏も Twitter 上でこのようにツイートしています。

ここでは、Generics はとても重要なものであるが、当たり前になってはいけないと言っています。

過剰な抽象化は、反ってコードを読みづらくします。筆者がこれまでほかの言語で見てきた Generics を使う問題のほとんどが、適切な設計をするための判断材料が十分にないということがほとんどでした。 結果的に、拡張性や可読性がなくなったり、作り直したりということになりました。最初から Generics を使うのではなくて、まず動くコードを書いてみる。同じロジックを書く機会に遭遇したとき Generics を検討してみましょう。本当にこのコードは Generics が必要なのか、早すぎる最適化ではないのかと常に考えましょう。

それでは、素敵な Go ライフを!

この記事が気に入ったらサポートをしてみませんか?