4/23に開催されたGo Conference 2022 Springで 「型パラメータが使えるようになったのでLINQを実装してみた」という発表をしました。

動画

Go界隈ではジェネリクス不要論を唱える人もいましたが、僕自身は欲しいと思っていました。 さすがに発表したLINQを常用したいとは思わないでしょうが、 ある程度の規模のGoのプロダクトをきちんと実装していたら、ジェネリクスが必要になる場面に出会っていたはずです。 よくあるのは、インターフェイスを実装した型のスライスに対する関数です。

https://go.dev/play/p/bf5-RgY6lfb

type Namer interface {
	Name() string
}

func QuoteName(n Namer) string {
	return fmt.Sprintf("「%s」", n.Name())
}

func PrintNames(ns []Namer) {
	for _, n := range ns {
		fmt.Println(QuoteName(n))
	}
}

type A struct{}

func (A) Name() string { return "A" }

func main() {
	as := []A{A{}, A{}, A{}}
	PrintNames(as) // cannot use as (variable of type []A) as type []Namer in argument to PrintNames
}

Goではインターフェイスで抽象化して関数を書けます。 この例では型ANamerとして扱えますが、[]A[]Namerとして扱えません。 スライスを詰め替えるか、型ごとに実装を用意することになってしまいます。

Go 1.18より前はコード生成で解決することもありましたが、 生成プログラムを別途用意しないとビルドできなかったり、 あるいは生成元データと生成後のコードを二重管理することになってしまいがちで、 個人的に好ましくないと思っていました。

Go 1.18以降では、型パラメータを使って次のように書けるようになりました。 https://go.dev/play/p/dQEAcJoz2dO

func PrintNames[T Namer](ns []T) {
	for _, n := range ns {
		fmt.Println(QuoteName(n))
	}
}

interfaceによる型制約は、旧ドラフトにあったコントラクトよりも、このような使い方にとても合っていると思います。

Goの型パラメータは、もう一歩使いやすくなってくれると嬉しい部分もありますが、 シンプルさと使いやすさのバランスがよく練られていると思います。

みなさんもどんどん使っていきましょう。