Go

Go 2のgenerics/contract簡易まとめ

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

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

また、Go 2 feedback in Japanese(contracts)に日本語でコメントしておくと、英語に翻訳して公式に伝えてくれるようなので、賛成または反対だけでもフィードバックしてください。

導入

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})

他の言語、例えばJavaやC++を経験した人は、なんで<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を使って必要な条件を表明します。コントラクトの本体には関数やメソッドと同じように、式や条件文などを書きます。ただし、これらのコードは型の検査に使うだけで、実行されることはありません。

contract equalable(t T) {
    t == t
}
contract comparable(t T) {
    equalable(T)
    t < t
}

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

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

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

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

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

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

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

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

コントラクトは演算子だけでなく、例えば特定のメソッドが必要な場合は以下のように書くこともできます。

contract stringable(x T) {
    var s string = x.String()
}

左辺値でstringと宣言することによって、String()の戻り値がstringであることを要求します(そうでなければコンパイルエラー)。なるほどと思う反面、通常はs := x.String()のように型を省略することが多いので、少々テクった書き方だなとは思いました。

interfaceとの違い

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

  • interfaceは常にポインタと型を持つけどジェネリクスは値型のまま使える
  • interfaceは実行時にメソッド解決だけどジェネリクスはコンパイル時に解決する?

contractの制限

コントラクトの本文に書くものは関数などと同じですが、型チェックが目的なので扱いは少し異なります。

  • contractが記述されたカレントパッケージの型や関数は参照できない
  • goto, break, continue, fallthroughなどの型を伴わない制御文は意味がない
  • contractはパッケージ直下にしか書けない(関数の中で宣言できない)

面白いコントラクトの例

ドラフトや他の記事で見かけた面白いコントラクトをいくつか紹介します。なるべく癖が分かり易そうなものを選びました。

型パラメータの部分適用

contract convertible(_ To, f From) {
    To(f)
}
func FormatUnsigned(type T convertible(uint64, T))(v T) string {
    return strconv.FormatUint(uint64(v), 10)
}

convertibleは型パラメータを2つ持ちますが、FormatUnsignedは型パラメータのうちTouint64で固定して、Fromを型パラメータTとして扱えるようにしています。

定数

型のない定数(0, "", falseなど)は別途contractに書いておかないと扱えないそうです。

contract add1(x T) {
    x = 1
    x + x
}

func Add1(type T add1)(a []T) {
    for i, v := range a {
        a[i] = v + 1
    }
}

構造体フィールド

構造体フィールドであっても同じように扱います。

contract client(x T) {
    var _ *http.Client = x.Client
}

type xxClient struct {
    Client *http.Client
}

このコントラクトは*http.ClientClient フィールドを持っていれば満たされます。

メソッド?

少し特殊な例。

type X struct {
    String func() string
}

contract stringable(x T) {
    var s string = x.String()
}

構造体のString func() stringフィールドでも、Go言語の構文としては同じなのでstringableを満たします。厳密にメソッドであることを要求したい場合は別の書き方をしなければなりません。

チャネル関連

Gophers(Slack)における@tenntennさんの発言よりいくつか。

// tは受信可能なチャネル
contract receivable(t T) {
    <-t
}

// tはポインタ
contract pointer(t T) {
    *t
}

// tはマップ; rangeだけだとスライスも含まれてしまうのでdelete()も必要
contract Map(t T) {
    for k, _ := range t {
        delete(t, k)
    }
}

感想と告知

他の言語と様子が全然違いますが、Goのジェネリクスについて雰囲気はつかめたでしょうか。下手なコントラクト書いてしまうと詰むよな、とか、色々思うところはありますけれども、概ね納得はしているので、まだドラフトですし個人的には今後の変更に期待しています。

また、9/27にGo 2 Draft Designsフィードバック会が開催されるようなので、参加できる方は参加したり、Go 2フィードバックドキュメント(contracts)にコメントを書いたりしてみてください。