Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Go 2のgenerics/contract簡易まとめ

この記事はQiitaで公開されていました

この記事はジェネリクスのドラフトを読んで、個人的に理解した内容をまとめたものです。ジェネリクスコントラクトは、現在まだ仕様のドラフトが出てきたところなので、今後変わっていく可能性が非常に高いです。

以下の内容は、ドラフトを読みながら書いているので、おそらく抜けている部分や理解不足なところはあると思います。特に、間違いがあれば修正しますので、この記事のコメント等で教えてください。原書は以下のリンクから。

導入

Goはこれまで、ジェネリクスも持っていないし継承もありませんでした。そのため、コレクションを扱う汎用的な方法は、interface{}などを使って多少ぎこちないけれどもうまくやるしかありませんでした。

例えばGo 1.11現在、標準の container/list はこんな感じです。

// 定義
package list

type Element struct {
    Value interface{}
}
func (e *Element) Next() *Element
func (e *Element) Prev() *Element

// 使い方
package main

l := list.New()
l.PushFront("test")
for e := l.Front(); e != nil; e = e.Next() {
    s := e.Value.(string)
    if s == "test" {
        ...
    }
}

元の型に戻すためには、扱う側で型アサーション(type assertion)しないといけません。リストとして動作はしていますが、もっと型の恩恵を受けたくなりますね。

また、標準の sort パッケージは、

func SearchFloat64s(a []float64, x float64) int
func SearchInts(a []int, x int) int
func SearchStrings(a []string, x string) int
type Float64Slice
type IntSlice
type StringSlice
func Float64s(a []float64)
func Ints(a []int)
func Strings(a []string)

など、よく使うプリミティブ型ごとに同じようなメソッドを提供してくれていますが、この辺りも、今後メンテナンスすることを考えるとつらいだろうなと思いますし、使う側も、例えば[]int32をソートしようと思った場合、

// これはできない
a := []int32{0, 1, 2}
sort.Ints(a) // cannot use s (type []int32) as type []int in argument to sort.Ints

// 詰め替えてあげる必要がある
a1 := make([]int, len(a))
for i := range a {
    a1[i] = int(a[i])
}
sort.Ints(a1)

のように、適切な配列へ詰め替えるか、sort.Interfaceを満たす型を自分で書くか、のどちらかが必要です。

ジェネリクスへの要望はずっと前からあったにも関わらず、言語の複雑さや実行速度などの影響から導入されてきませんでしたが、記事の最初で紹介したように、Go 2 Draft Designsジェネリクスのドラフトが上がってきました。Go 2と呼称していますが、実際はGo 1.15または1.16あたりを指しているようです。全く別の言語になるわけではありませんし、互換性も今まで通り維持されます。

追加される文法

ドラフトによると、ジェネリクスのために、型パラメータ型引数コントラクトが追加されます。

型パラメータと型引数

ジェネリックな型や関数は、名前の直後にカッコとtypeを使って実装します。型名の部分を型パラメータ (type parameter)と呼びます。

type List(type T) []T

func (l *List(T)) PushBack(x T)

type IntList = List(int) // typealias

func Keys(type K, V)(m map[K]V) []K

ジェネリックな型や関数を使う側は型名を渡します。型パラメータに渡す型は、型引数 (type argument)と呼びます。

var l List(string)
l.PushBack("hello")

// 引数から型引数がわかる場合は省略可能
keys := Keys(map[string]int{"A": 1, "B": 2})

他の言語、例えばJavaC++を経験した人は、なんで<T>[T]じゃないの、と思いますが、以下のようなケースで言語パーサが複雑になるからだそうです。

v := F<T>
n := f(a<b, c>d)

これらジェネリックな関数は、型パラメータの部分だけを適用することも可能です。上で挙げたsortパッケージの例をGo 2のジェネリクスで再定義すると、こんな感じでしょうか。

func Sort(type T)(a []T)
var Ints = Sort(int)
var Strings = Sort(string)

コントラクトの追加

型パラメータだけでは、例えばSort(type T)(a []T)の実装はできません。a[i]の値がa[j]と比べて大小どちらなのかを比較する方法が必要です。または実装によっては、sort.InterfaceのようにLess()メソッドを要求するかもしれません。しかし型パラメータだけでは必要な条件を表明することができません。

このため、ドラフトでは、contractを使って必要な条件を表明します。コントラクトにはinterfaceと同じように、メソッドシグネチャを書けますし、型名を書くこともできます。コントラクトの型パラメータは複数書くことができるので、区別のためメソッド名の前に型パラメータが必要です。

memo: ,で区切るとOR条件、別に分けるとAND条件

contract equaler(T) {
    T int, Equal(T) bool
}
contract comparer(T) {
    equaler(T)
    T Less(T) bool
}

contractの名前はよく小文字で表記されますが、型チェックで使うものなので、他のパッケージへエクスポートしている必要は 例を眺めた限りではおそらく ありません。(大文字開始の名前にすればエクスポート可能ですが)

こんな面倒なもの導入しなくても、ジェネリック関数の本文から必要な条件を抽出することもできるんじゃないのと思ってしまいますが、それだと内部実装を少し変更するだけでコントラクトも変わってしまうしエラーメッセージも不恰好になるため、コントラクトとして明記するように設計したようです。

型パラメータにコントラクトを追加

contractを参照する側は、型パラメータの後に続けてコントラクトを書きます。

func Sort(type T comparer(T))(a []T)

// contractのTは省略しても良い
func Sort(type T comparer)(a []T)

// 型パラメータが複数ある場合(この2つは同等)
func F(type T1, T2 comparer)(t1 T1, t2 T2)
func F(type T1, T2 comparer(T1, T2))(t1 T1, t2 T2)

コントラクト付きの型パラメータに、型引数を与えたコードをコンパイルすると、型引数がコントラクトを満たしているかのチェックがコンパイラで行われます。

interfaceとの違い

とりあえず気づいたところを2つ。他にもあるかもしれません。

  • interfaceは常にポインタと型を持つけどジェネリクスは値型のまま使える
  • interfaceは実行時にメソッド解決だけどジェネリクスコンパイル時に解決する?
  • 型の混在したコンテナのようなものはinterfaceでなければできない?

感想と告知

他の言語と様子が全然違いますが、Goのジェネリクスについて雰囲気はつかめたでしょうか。最初のバージョンと比べてだいぶ理解しやすくなったと思いますし、インターフェイスとの使い分けも分かりやすくなったんじゃないかなと感じました。