golangのジェネリクス
これから実装される予定のgolangのジェネリクスで遊んでみました。実装されたら普通に業務でも使っていきたいなーってくらいには色々と便利になりそうな感じがしました!
概要
golangのジェネリクスでできることをざっとまとめるとこんな感じ
できるようになること
- 書き方は
func F[T any](p T){...}
- どの型でも取れる値は新しい予約語
any
を使う - 型パラメータリストが定義できる
type A[T any] []T
- それぞれの型パラメータには型制限をかけることができる
func F[T Constraint](t T){...}
できないこと
- 共変性と反変性は持たない
- メソッドにはジェネリクスは使えない
- 可変長引数は使えない
実際に使ってみる
golangにはfunkパッケージを使わないと使えないmapとforeachを実装してみました。
package main
import (
"fmt"
)
type A struct {
val string
}
func(a A) PrintA() {
fmt.Printf("value: %s\n", a.val)
}
func ForEach[t1 any](l []t1, f func(t1)) {
for _, n := range l {
f(n)
}
}
func Map[t1, t2 any](l []t1, f func(t1) t2) []t2 {
dst := make([]t2, len(l), len(l))
for i, n := range l {
dst[i] = f(n)
}
return dst
}
func main() {
intList := []int{0, 1, 2, 3, 4, 5}
strList := []string{"a", "hoge", "huga"}
AList := []A{A{"a"}, A{"b"}}
intListMap := Map(intList, func(i int) int {return i + 1 })
strListMap := Map(strList, func(s string) string {return s + "foo"})
AListMap := Map(AList, func(a A) A {return A{a.val + "A"}})
fmt.Printf("int: %v, type: %T\n", intListMap, intListMap) // int: [1 2 3 4 5 6], type: []int
fmt.Printf("string: %v, type: %T\n", strListMap, strListMap) // string: [afoo hogefoo hugafoo], type: []string
fmt.Printf("A: %v, type: %T\n", AListMap, AListMap) // A: [{aA} {bA}], type: []main.A
ForEach(AList, func(a A) {a.PrintA()})
/*
value: a
value: b
*/
}
コードの解説などは割とシンプルなので不要かなと思います。
funkパッケージを使うと戻り値がinterface{}で返ってくるので型チェックが必要になりますが、それが必要なくなるのでめちゃくちゃ嬉しい。それにreflectパッケージは使わなくなるので実行速度も上がるのかな?と思っています。(すみません、これに関しては調べられてはいないので僕の予想になってしまいます。)
ちょっとしんどいなーと思ったのはMap(strList, func(s string) string {return s + "foo"})
みたいな感じで引数の中に func(s string) string {...}
みたいに書かないといけないので可読性下がるなーというのは感じました。
scalaとかだと a.map(_ => _ +1)
みたいに書けるのでこの辺は辛みが若干ある感じがします。
とはいえ、それを有り余っていろんなことできそうだなーと感じました。
独自の型を実装してもきちんと動くので嬉しいですね。
型制限を付けてみる
このメソッドを持ってる型だけは受け入れるみたいなことができます。
type Addable[t any] interface {
Add(v1 t, v2 t) t
}
type A struct {
val string
}
func(a A) Add(v1 A, v2 A) A {
return A{v1.val + v2.val}
}
/* メソッドにジェネリクスは使えないので、これはできない
func(a A) Convert[t any](f func(A) t) t {
return f(a)
}
*/
func Sum[t Addable[t]](l []t) t {
var sum t
for _, v := range l {
sum = v.Add(sum, v)
}
return sum
}
func main() {
AList := []A{A{"a"}, A{"b"}}
/*
methods cannot have type parameters
a := A{"a"}
a.Convert(func(a A) string { return a.val})
*/
sum := Sum(AList)
fmt.Printf("sum: %v, type: %T\n", sum, sum)
}
まず、メソッドにジェネリクスを使うことはできないので A
を任意の型に変換するみたいなことはできないです。
そして、ここでやっているのは、 Add(v1 t, v2 t)
を満たす型のみを型パラメータに受け入れる関数 Sum[t Addable[t]](l []t) t
を実装しています。やっていることは、t型の配列を引数に取り、実装されたAdd関数を実行してその合計を求める的な簡単なものです。便利。基盤の実装とかがかなり表現の幅が広がりそうだなーと感じました。
最後に
今回は簡単ですが、今後実装されそうなgolagnのジェネリクスについて触れてみました。個人的にはgolangで物足りなかったコードの表現力がめっちゃ上がりそうだなーと感じています。実装されるのが楽しみです!
参考文献
Type Parameters - Draft Design
連結リストの実装でGo言語のジェネリクスのドラフトを触ってみる
これは参考文献ではないですが
元のコード