Go1.20でcomparable制約はどう変わるのか？

## はじめに

どうも[ナレッジワーク](https://tenn.in/kw)の[tenntenn](https://tenn.in)です。

Goには、1.18でジェネリクス（型パラメータ）が導入されました。ジェネリクスの導入と共に、インタフェースを型パラメタに対する型制約として利用し、新しく型セットという概念が入りました。

型制約はインタフェースとして宣言可能ですが、『比較可能な型』のような制約のもつ型セットが無限集合になる、かつ現時点でのインタフェースでの表現では難しい制約を組み込みで提供することで解決しています。

たとえば、『比較可能な型』を表現する制約として`comparable`が導入されました。ここでは、Go1.18で提供された`comparable`についてと、それがGo1.20でどう変わるのかを解説します。

なお、本記事で扱う内容は、Go1.20でリリースとしていますが、今後の変更によっては変わる可能性があります。しかし、すでに変更はRC1版としてリリースされているため、入る可能性は高いでしょう。

## Go1.18で導入されたcomparableとは？

`==`演算子で比較したり、マップのキーにするためには、それらの型が比較可能である必要があります。たとえば、`int`型は比較可能な型ですが、`int`型のスライスｈ（`[]int`）は比較できません。また、関数なども比較できませんし、比較できない型をフィールドや配列の要素として持つ型も比較できません。

Go1.18のリリースに合わせて公開された[golang.org/x/exp/maps](https://golang.org/x/exp/maps)では、`Keys`関数や`Values`関数など、Goエンジニアが待っていた機能が提供されています。

たとえば、`Keys`関数はマップのキーをスライスで取得できますし、`Values`関数は値をスライスで取得できます。ジェネリクスの導入により、`for range`文でループする必要がなくなりました。

`maps.Keys`関数は次のように宣言されています。

```go
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
	r := make([]K, 0, len(m))
	for k := range m {
		r = append(r, k)
	}
	return r
}
```

このコードをいきなり説明するには難しすぎるため、まずは`maps.Keys`関数より機能が劣る`MyKeys`関数を作って、徐々に改善していきながら説明していきましょう。

最初はジェネリクスを使わないバージョンを考えましょう。`map[string]int`型を引数にとって、キーの一覧を`[]string`型で取得するような関数にしてみます。

これはジェネリックな関数（ジェネリクスを使った関数）を定義する際にも使えるテクニックですが、いきなりジェネリクスを使うのではなく、特定の具象型でまずは書いてみると整理しやすくなるのでおすすめです。

さて、`map[string]int`型版の`MyKeys`関数は次のようになります。

```go
// ver. 1
func MyKeys(m map[string]int) []string {
	r := make([]string, 0, len(m))
	for k := range m {
		r = append(r, k)
	}
	return r
}
```

多くのGoエンジニアが書いてきた、いつものコードですね。

これをキーと値の型を型パラメータにして、汎用化してみましょう。

```go
// ver. 2
func MyKeys[K comparable, V any](m map[K]V) []K {
	r := make([]K, 0, len(m))
	for k := range m {
		r = append(r, k)
	}
	return r
}
```

`map[string]int`型版と比べると、`string`型だったところが型パラメータ`K`に、`int`型だったところが型パラメータ`V`になっています。型パラメータは実際に関数を使う際に、`MyKeys[string, int](m)`のように型引数を指定して使用します。この場合は、`K`が`string`型、`V`が`int`型になります。もちろん、型推論が働くので、`MyKeys(m)`と型引数を省略することもできます。

ここで、`MyKeys`関数の`[K comparable, V any]`の部分は型パラメータを宣言しています。`comparable`と`any`は、それぞれ型パラメータ`K`と`V`に対する型制約です。

型パラメータ`K`に指定できる型は、比較可能である必要があるため、`comparable`制約が使われています。

また、`any`はGo1.18で導入された`interface{}`の型エイリアスです。`interface{}`（empty interface)と同様の意味になります。つまり、任意の型を許す型制約（制約がない）です。

~~実は、`MyKeys`関数のver. 2では、任意のマップ型を引数に渡すことはできません。たとえば、次のように宣言されたマップを渡そうとするとコンパイルエラーになります。~~
追記（2022/12/13）：この記述は誤りです。修正中です。

```go
type MyMap map[string]int
```

そこで、`MyKeys`を次のように改良することでDefined typeなマップも受け付けれるようにします。

```go
type Map[K comparable, V any] interface {
    ~map[K]V
}
// ver. 3
func MyKeys[M Map[K, V], K comparable, V any](m M) []K {
	r := make([]K, 0, len(m))
	for k := range m {
		r = append(r, k)
	}
	return r
}
```

制約`Map`は、次のように省略してインラインで書けます。

```go
// ver. 3
func MyKeys[M ~map[K]V, K comparable, V any](m M) []K {
	r := make([]K, 0, len(m))
	for k := range m {
		r = append(r, k)
	}
	return r
}
```

制約に`~map[K]V`と書くと、`interface{~map[K]V}`と書いた場合と同じになります。

これでようやく`maps.Keys`関数と同じになりました。実は、`maps.Keys`関数でもGo1.19まででは引数に指定できないマップが存在しました。

言語仕様で規定している比較可能(comparable）には、インタフェースを含みます。インタフェースの場合、そのインタフェースを実装している動的型（dynamic type）が比較可能ではない場合もあります。その場合は比較演算子やマップのキーに指定するとパニックを起こします。

一方、Go1.19までの`comparable`制約は、インタフェースを許可していませんでした。つまり、`map[any]string`型のようなマップは、型としては言語仕様に沿っており、コンパイルは通りますが、`comparable`制約を満たしません。

つまり、このようなマップは`maps.Keys`関数の引数に指定できなかったのです。

Go1.19までの`comparable`の種類については[『Go言語のcomparableには3つの意味がある』](https://zenn.dev/nobishii/articles/3-means-of-go-comparable)という記事が分かりやすいでしょう。

## Go1.20でこう変わる

[Issue#56548](https://go.dev/issue/56548)で提案されたプロポーザルによって、インタフェースも`comparable`制約を満たすようになります。つまり、`maps.Keys`関数の引数に`map[any]string`型のようなインタフェースをキーに持つマップを指定できるようになります。

Go1.20のRC1版（リリース候補版）が公開されているため、次の手順でgo1.20rc1コマンドをインストールすると試せます。

```
$ go install golang.org/dl/go1.20rc1@latest
$ go1.20rc1 download
```

なお、go1.20rc1コマンドは通常のgoコマンドと同様に扱えます。

## インタフェースを除く比較可能な型であることを保証するには？

Go1.19までの`comparable`制約の特性を使って、ある型`T`がインタフェースを除く、比較可能な型であることを保証するためには、次のように記述できました。

```go
func isComparable[_ comparable]() {}

var _ = isComparable[T]
```

型パラメータを1つ取る`isComparable`関数を作り、型制約を`comparable`とすることで、`isComparable`関数をインスタンス化する際に指定する型引数を比較可能でかつインタフェースではない型に絞る方法です。

`var _ = isComparable[T]`は、型`T`でインスタンス化を行っているため、型`T`が`comparable`制約を満たさないとコンパイルが通りません。

そのため、次のような型を考えた場合、型`OK`はコンパイルは通りますが、型`NG`はコンパイルが通りません。

```go
type OK struct {
    n int
}

type NG interface {
    F()
}
```

しかし、Go1.20では、インタフェースも`comparable`制約を満たすため、型`NG`も`comparable`制約を満たすようになってしまいます。プロポーザルにはこの件に関する[質問](https://github.com/golang/go/issues/56548#issuecomment-1317673642)が出ています。

GoチームでGoの最初の設計者の1人であるRobert Griesemer氏によると、次のような関数を定義すると同じようなチェックができると返信しています。

```go
func TisComparable[P T]() {
	_ = isComparable[P]
}

func isComparable[_ comparable]() {}
```

質問者はこれに対して、「Clever.」と返信しています。筆者はここを読んだだけではさっぱり分からず、何度も読み直してやっと理解しました。

まずは、型`T`が次のように構造体で比較可能であった場合を考えます。

```go
type T struct {
    n int
}
```

`TisComparable`関数の型パラメータと型制約に`[P T]`と記述されているため、型`T`は制約として扱われます。この場合、型`T`はインタフェースではないため、`interface{T}`と書いた場合と同様になります。つまり、この型制約は型`T`だけを許可する制約となります。ちなみに、`interface{int | string}`のように書くと`int`型または`string`型のどちらかとなります。

型パラメータ`P`は制約`interface{T}`を満たす型しか許されないため、型引数として指定できるのは型`T`だけとなります。そのため、`_ = isComparable[P]`は`_ = isComparable[T]`と同義となるため、型`T`は比較可能（ここではインタフェースも許される）となりコンパイルが通ります。

では、次に型`T`が次のようなインタフェースの場合を考えます。

```go
type T interface {
    F()
}
```

ここでの型`T`は、インタフェースであるため`[P T]`のように型制約に用いた場合、素直にそのまま解釈されます。つまり、型パラメータ`P`には、メソッド`F`を実装している型であればどんな型でも型引数として指定できます。つまり、メソッド`F`があれば、スライスでも問題ありません。

そのため、`_ = isComparable[P]`の`P`がスライスなど、比較できない型になる可能性があるため、この場合はコンパイルエラーになります。

もし、比較可能であるインタフェースを許可したい場合は、次のような型`T`であればコンパイルエラーとなりませんが、このような型は現時点では型制約にしか使えません。

```go
type T interface {
    comparable
    F()
}
```

なるほど。とても難しいですね。

## おわりに

本記事では、Go1.20で導入される新しい`comparable`制約の定義について説明しました。ジェネリクス周りはなかなか難しいことが多いため、理解するのは大変ですが理解できるととても楽しいです。

[Gopher塾](https://gopher.school)でも、そのうちジェネリクスを扱いたいなと考えておりますので、開催することが決まったらconnpassやHPで告知します。