# Go で始める Generics 勉強会 2022年2月23日 静岡大学プログラミングサークル SZPP earlgray(@earlgray329) ## 0. 準備編 2022年2月23日(水)の現時点で、まだ Go1.18 はリリース前の RC 版でしか提供されていません。 今回は goenv とか特に使わずに `/usr/local/go/bin/go` に RC 版をインストールしますが、stable じゃないと嫌という方は docker 使うなりしてください。 **インストール(Unix/Linux)** (1) go1.18rc1 のインストール ```bash $ wget https://go.dev/dl/go1.18.linux-amd64.tar.gz $ sudo rm -rf /usr/local/go # 古いバージョンの go を削除 $ sudo tar -C /usr/local -xzf go1.18.linux-amd64.tar.gz $ go version go version go1.18rc1 linux/amd64 # command not found になったら $ echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile $ exec $SHELL -l ``` (2) gopls とかのアップデート VScode: ctrl+shift+p でコマンドパレットを開いて `Go: Install/Update Tools` を実行 ## 1. Go1.18 で到来するもの Go 史上最大のアップデートです。 詳しくは [Go 1.18 Release Notes](https://tip.golang.org/doc/go1.18) をみてね! **主な追加点** 1. **Generics**(今日はこれ) 2. Fuzz Test ### Generics 公式チュートリアル: <https://go.dev/doc/tutorial/generics> 今回の目玉です。 あるまかんの C++ のコードでよく見る `template<T>` が Go でもできるようになります。 **Example** ```go= // v の要素全てが f を満たしたら true を返す func all[T any](v []T, f func(*T) bool) bool { for _, elem := range v { if !f(&elem) { return false } } return true } ``` ### Fuzz Test 公式チュートリアル: <https://go.dev/doc/tutorial/fuzz> **Go におけるテストについて**  go1.17 までで人々がやってきた `func Test○○(t *testing.T)` は全て **Unit test** です。go1.18 からは **Fuzz test**が到来しますが、そもそも Fuzz test ってなんだという話なので比較をしながら簡単に説明します。 **テストの対象**  テストする対象の関数は `checkedDiv` 関数です。これは $a / b$ の結果が存在したらその値を返し、存在しなければ `nil` を返します。 ここではあえて適当な実装にしています。 ```go // nil はどこ・・・ func chackedDiv(x, y int) *int { res := x / y return &res } ``` **Unit-test とは**  プログラマがテストケースとして入力とそれに対する期待値(出力)を用意するもの。関数の動作が正しいかをテストするときはこれ。 **Example** ```go= func TestCheckedDiv(t *testing.T) { testcases := []Testcase[Pair[int, int], int]{ {Pair[int, int]{1, 1}, 1}, {Pair[int, int]{8, 2}, 4}, {Pair[int, int]{3, 6}, 0}, } for i, testcase := range testcases { res := chackedDiv(testcase.in.first, testcase.in.second) fmt.Printf("Test %d => %v\n", i, func() string { if *res == testcase.expect { return "passed:)" } else { return "failture:(" } }()) } } ``` ただ、見ればわかるようにゼロ徐算のテストができてないのでこれはまずいです。 **Fuzz test とは**  プログラマがシード(テストケース)を用意して、そのシードを元にテストケースを自動生成してテストすること。異常がないか確かめるときはこれ。 **Example** ```go= func FuzzCheckedDiv(f *testing.F) { f.Fuzz(func(t *testing.T, in1, in2 int) { t.Log(in1, in2) _ = chackedDiv(in1, in2) }) } ``` これを実行すると `47 0` のケースで撃墜していることがわかります。 ```go= $ go test -test.v -fuzz Fuzz === FUZZ FuzzCheckedDiv warning: starting with empty corpus fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0) fuzz: elapsed: 0s, execs: 2 (132/sec), new interesting: 1 (total: 1) --- FAIL: FuzzCheckedDiv (0.02s) --- FAIL: FuzzCheckedDiv (0.00s) main_test.go:44: 47 0 testing.go:1350: panic: runtime error: integer divide by zero ``` ゼロ徐算とか配列参照とかはかなりやりがちなので、Fuzz test で見つけることができると嬉しいですね。 ## 2. 実践!Generics では実際に Go の Generics に触っていきましょう。 ### (1) 型パラメータの宣言 型パラメータの宣言は以下のようにして行います。 **関数** ```go= func myFunc[{型パラメータ名} {型制約}](引数*) ``` **構造体** ```go= type myStruct[{型パラメータ名} {型制約}] struct ``` ### (2) max 関数を実装してみよう とりあえず型パラメータの宣言はできたので Generics を使うことができるようになりました。 では早速、スライスを渡すと最大値を返す max 関数を generics を使って実装してみましょう。 **お気持ちとしての実装例(max 関数)** ```go= package main import "fmt" func max[T any](a []T) T { var maxValue T = a[0] for _, elem := range a[1:] { if elem > maxValue { maxValue = elem } } return maxValue } func main() { v := []int{7, 0, 0, 1, 0, 0, 9, 8} fmt.Println("Max is", max(v)) } ``` しかし、残念ながらこのコードだとコンパイルは通りません。 ``` $ go run . # go-playground ./main.go:4:2: imported and not used: "constraints" ./main.go:23:6: invalid operation: cannot compare elem > maxValue (operator > not defined on T) ``` これは `T` の型制約が原因です。 ### (3) 型制約ってなあに 型制約は型パラメータが満たすべき制約のことを言います。主な型制約を以下に列挙します。 |型制約|説明| |---|---| |`any`|`interface{}` のエイリアスです。なんでも受け入れます。| |`comparable`|組み込みの型制約です。`==` と `!=` を実装している型が受け入れられます。| |`constraints.Ordered`|`constraints` パッケージの型制約です。`<` と `>` を実装している型が受け入れらます。| |`constraints.Integer`|`constraints` パッケージの型制約です。整数型が受け入れらます。| |`type MyInterface interface{}`|その interface を実装している型が受け入れられます。| `constraints` パッケージには、以上の他にも複素数の型制約 `Complex` や小数の型制約 `Float` などがあります。 これを踏まえると、max 関数の型パラメータが any だとダメな理由がわかると思います。 any だとなんでも受け入れられてしまいますが、ポインタ型とか構造体とかは比較することができません。これらのように、比較できない型を防ぐために、前もって「比較ができる型しか受け付けません!」と型制約を設ける必要があります。 結論ですが、型パラメータを `constraints.Ordered` に変えてあげることで max 関数は動作します。 **修正後の max 関数(diff)** ```diff= + import "constraints" + func max[T constraints.Ordered](a []T) T { - func max[T any](a []T) T { ``` **実行結果** ``` $ go run go-generics.go Max is 9 ``` :::info ちなみに、実は go1.18 から標準ライブラリの `interface{}` は全て `any` に置き換えられています。 ```go func Println(a ...any) (n int, err error) ``` ::: ### (4) sum 関数を実装してみよう ここまで「型パラメータの宣言」と「型制約」を学んできたので、演習を行いましょう。スライスを渡すと総和を求めて返す sum 関数を実装してみてください。 **お気持ちとしての実装例(sum 関数)** ```go= func sum[T any](v []T) T { sumValue := 0 for _, elem := range v { sumValue += elem } return sumValue } ``` お気持ちとしては上記のようなコードが書きたくなりますが、例の如く動作はしません。 ``` $ go run . # go-playground ./main.go:20:3: invalid operation: sumValue += elem (mismatched types int and T) ./main.go:22:9: cannot use sumValue (variable of type int) as type T in return statement ``` 今回は少しばかり問題点が多いので一つ一つ見ていきましょう 1. `sumValue := 0`  Go だと型を指定しない限り整数リテラルは int 型として解釈されるので、ここでは `sumValue` は int 型になってしまいます。これは T 型ではないのでアウトです。 2. `[T any]`  unsafe でない限りポインタに加算はできませんし、構造体はそもそもできないのでアウトです。 問題1は go の `var` を用いた変数宣言の性質を利用すれば解決できそうです。go では `var hoge int` のように特に初期値を指定しない限り全て **ゼロ値** で初期化されます。今回は `var sumValue T` で 0 で初期化することができます。 問題2は **型セット** を使えば解決できそうです。 ### (5) 型セットってなあに 型セットは詰まるところ↓です。 **Addable 型制約** ```go= // + の型制約 type Addable interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64 } ``` このように型制約を満たすプリミティブ型をパイプで繋いで列挙したものが型セットになります。上記の例だと、足し算ができる型の制約となっています。 ちなみに、型セットの要素として型制約を用いることもできます。 `constraints` パッケージにいい感じに集約されているので、それを使うと以下のようにコンパクトにできます。 **Addable 型制約** ```go= type Addable interface { constraints.Integer | constraints.Float } ``` **~ ってなんだ** `~int` の `~` は `int` のエイリアスも受け入れるという意味です。具体的には、`type MyInt int` みたいなやつを渡すことができるようになります。 あんまり意識しないがちですが、go ではエイリアスの対象とする型が同じでも、エイリアスが違うと別物として扱われます。例えば以下のようなコードはコンパイルが通りません。 ```go= type MyInt1 int type MyInt2 int type Addable interface { int } func add[T Addable](a, b T) T { return a + b } func main() { a, b := MyInt1(1), MyInt2(2) fmt.Println(add(a, a)) // MyInt1 は int じゃないので NG fmt.Println(add(a, b)) // MyInt1 != MyInt2 なので NG fmt.Println(add(b, b)) // MyInt2 は int じゃないので NG } ``` `MyInt1 != MyInt2` はそれはそうなのですが、エイリアスの対象とする型が同じ(=型制約を満たしている)なのに違う扱いされるのは困りますね。そこで `~` を使います。 ```diff= + type Addable interface { ~int } - type Addable interface { int } - fmt.Println(add(a, b)) // MyInt1 != MyInt2 なので NG ``` ### (5) add 関数を実装してみよう 2 **動く実装例** ```go= type Addable interface { constraints.Integer | constraints.Float } func zero[T any]() T { var z T return z } func sum[T Addable](v []T) T { sumValue := zero[T]() for _, elem := range v { sumValue += elem } return sumValue } func main() { v1 := []int{7, 0, 0, 1, 0, 0, 9, 8} v2 := []float64{7.0, 0.0, 0.0, 1.0, 0.0, 0.0, 6.0, 0.0} fmt.Println(sum(v1)) fmt.Println(sum(v2)) } ``` **実行結果** ``` 25 14 ``` ### (6) 演習 1. JavaScript の `Array.prototype.filter()` を作ってみよう <https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter> ```go= func Filter[T any](a []T, f func(*T) bool) []T { b := make([]T, 0) for _, elem := range a { if f(&elem) { b = append(b, elem) } } return b } ``` 2. JavaScript の `Array.prototype.map()` を作ってみよう <https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map> ```go= func Map[T, U any](a []T, f func(*T) U) []U { b := make([]U, len(a)) for i, elem := range a { b[i] = f(&elem) } return b } ``` 3. C++ の `std::queue` を作ってみよう ```go= type Queue[T any] struct { head, tail int a []T } func NewQueue[T any]() *Queue[T] { return &Queue[T]{ a: make([]T, 200_000), } } func (q *Queue[T]) Push(value T) { q.a[q.tail] = value q.tail++ } func (q *Queue[T]) Pop() { q.head++ } func (q *Queue[T]) Front() T { return q.a[q.head] } func (q *Queue[T]) Size() int { return q.tail - q.head } ``` ## 3. Generics と interface{} って何が違うの・・? ### そもそも `interface{}` ってなんですか梶さん ### `interface{}` で add 関数を無理やり実装するとこうなる ## 4. まとめ