Go言語のBasic Interfaceはcomparableを満たすようになる(でも実装するようにはならない)
前回の記事Go言語のcomparableには3つの意味があるにおいて、Go1.19までの言語仕様のcomparableと型制約のcomparableは指す範囲が異なるということを説明しました。たとえば、any型はGo1.19言語仕様上comparableですが、comparable型制約を満たしていませんでした。
このギャップをなくすProposalがacceptされそうです。今回はその内容を説明します。
追記 2023/02/23
このProposalは採用されて、Go1.20で実装されました。この記事の内容は基本的にGo1.20の言語仕様において正しいです。一部、言語仕様書が更新される前に記述した部分があるので仕様の用語をちゃんと使えてない部分があります。
言語仕様としての理屈にそれほど関心がない読者の人は要約だけ読めば十分だと思います。
要約
- unionsをふくまないinterface型(basic interface)について、Go1.18時点では
comparable型制約を満たさないようになっていた - このproposalが実装されると、basic interfaceは
comparable型制約を満たすことができるようになる - 特別な場合として、
anyはcomparable型制約を満たすようになるので、次のようなコードが書けるようになる(今は書けない)
func f[T comparable](T) {}
func main() {
var x any
f(x)
}
用語の整理
authorのgriesemerさんは議論のために次の用語を用いています。
| 用語 | 前回の記事における対応する用語 | 意味 |
|---|---|---|
| spec-comparable | comparable(言語仕様) | 言語仕様上==を使ってもコンパイルができるすべての型 |
| strictly comparable | comparable(型制約) |
==でpanicせずに比較できる型 |
前回の記事のvenn図とは次のように対応します:

proposalで何が変わるのか
現在の状態(Go1.18)
Go1.18では、あらゆる型制約Cについて、
- 型
TがCを実装する(implement) - 型
TがCを満たす(satisfy) - 型
TがCの型集合に属する
という3つの文章は全く同じ意味です。
そしてcomparableの型集合は、strictly comparableな型全体からなる集合です。つまり、comparable型のうち、インタフェース型を含まない型です。より正確には、
- Boolean, 数値, 文字列, ポインター, チャネル型
- 全てのフィールドがstrictly comparableであるような構造体型
- 要素型がstrictly comparableであるような配列型
- 型集合の要素が全てstrictly comparableであるような型パラメータ型
だけがcomparableの型集合に属します。これを示すのが次のサンプルコードです。
func f[T comparable](T) {}
type S1 struct {
int
}
type S2 struct {
fmt.Stringer
}
func main() {
// spec-comparableな非インタフェース型なのでstrictly comparableである
f(1)
// spec-comparableだがインタフェース型なのでstrictly comparableではない
var x any
f(x) // compile error
// strictly comparableな型であるintのみをフィールドにもつstruct型はstrictly comparableである
var s1 S1
f(s1) // OK
// strictly comparableではないfmt.Stringerをフィールドに持つstruct型はstrictly comparableではない
var s2 S2
f(s2) // compile error
}
Proposal採用後の状態(Go1.xx)
Proposal採用後は次のようになります。
- 型
TがCを実装する(implement) - 型
TがCの型集合に属する
の2つの文章は全く同じ意味です。この点はGo1.18と変わりありません。
しかし、
- 型
TがCを満たす(satisfy)
はこの2つよりも広い場合にあてはまることがあります。
また、先程のサンプルコードはすべてコンパイルできるようになります(執筆時点ではできません)
func f[T comparable](T) {}
type S1 struct {
int
}
type S2 struct {
fmt.Stringer
}
func main() {
f(1)
var x any
f(x)
var s1 S1
f(s1)
var s2 S2
f(s2)
}
一方、次のようなコードはGo1.18でもProposal採用後でもコンパイルできません。
func f[T comparable](T) {}
func main() {
var s []int
f(s) // compile error: sliceはspec-comparableですらない
}
型制約を満たす(satisfy)の定義
- 型
TがCを実装する(implement) - 型
TがCを満たす(satisfy)
この2つは異なる意味を持つようになると書きました。では、型TがCを満たす(satisfy)とは正確にはどのような意味になるのでしょうか?
proposalによると、型Tが型制約Cを満たす(satisfy)のは次の2つのいずれかのときです。
- 型
TがCを実装する(implement)とき -
Cがinterface{comparable; E}の形で書けて、Tがspec-comparableであり、かつTがEを実装する(implement)とき- ここで、
Eはbasic interfaceつまりunionsを含まないinterfaceであるものとする
- ここで、
具体例を見る前に、なぜこの2つを別概念にする必要があるかを説明します。
なぜimplementとsatisfyを別概念にする必要があるか
要請として、anyがcomparable型制約を満たす(satisfy)ようにしなければいけないとしましょう。
その上でsatisfyとimplementが全く同じ意味であると仮定すると、次のようにまずいことになります。
守るべき前提として、[]intのようにspec-comparableではない型がcomparable型制約を満たさないようにしなければいけません。
ここで、[]intはanyを実装することに注意すると、次のようなことが言えます。
-
[]intはanyを実装する (anyの定義から当然) -
anyはcomparableを実装する (要請と、satisfyとimplementの同義性から) -
[]intはcomparableを実装しない (守るべき前提)
すると、[]intはanyを実装し、anyはcomparableを実装するのに、[]intはcomparableを実装しないという奇妙な結論になってしまいます。
つまり、implementするという関係は本来推移法則を満たさないといけないはずなのに、推移法則を満たさない結果になってしまっています。
これは言語仕様として困るので、ここまでで使った前提のどれかは諦めないといけません。
最も諦めがつくのは、implementとsatisfyが同義であるという前提です。
具体例
anyはcomparableを満たす
proposalによると、型Tが型制約Cを満たす(satisfy)のは次の2つのいずれかのときです。
- 型
TがCを実装する(implement)とき -
Cがinterface{comparable; E}の形で書けて、Tがspec-comparableであり、かつTがEを実装する(implement)とき- ここで、
Eはbasic interfaceつまりunionsを含まないinterfaceであるものとする
- ここで、
Cにcomparableを代入して整理すると次のようになります。comparableはbasic interfaceであるanyをつかって次のように書けることに気をつけます:
// これは疑似コードです
type comparable interface {
comparable
any
}
型Tが型制約comparableを満たす(satisfy)のは次の2つのいずれかのときである:
- 型
Tがcomparableを実装する(implement)とき -
Tがspec-comparableであり、かつTがanyを実装する(implement)とき
どんな型もanyを実装することを使えば、この条件は次と同じことです:
型Tが型制約comparableを満たす(satisfy)のは次の2つのいずれかのときである:
- 型
Tがcomparableを実装する(implement)とき -
Tがspec-comparableであるとき
この2つのうち前者は後者に含まれますから、結局、
型Tが型制約comparableを満たす(satisfy)のはTがspec-comparableであるときである
と簡単にできます。
Tにanyを代入すればanyはspec-comparableなので、anyはcomparableを満たします。
より複雑な制約
type C interface {
comparable
String() string
}
fmt.Stringerがこれを実装するかを考えてみましょう。fmt.Stringerはcomparableを実装しません。しかし、fmt.Stringerはspec-comparableであり、かつString() stringを実装します。したがって fmt.StringerはCを実装はしませんがCを満たします。
[]intはどうでしょうか?[]intはspec-comparableではないのでCを満たしません。
ある型がstrictly comparableであることをコンパイル時にチェックする方法
Go1.18ではstrictly comparableな型だけがcomparableを満たしていたので、panicせずに==, !=で比較できる型であることをコンパイル時にチェックできました。
proposal採用後は、ある型Tがpanicせずに==, !=で比較できる型であるのをコンパイル時にチェックできなくなるのでしょうか?
これにはauthorのgriesemerさんが次のような方法を提示しています。
// we want to ensure that T is strictly comparable
type T struct {
x int
}
// define a helper function with a type parameter P constrained by T
// and use that type parameter with isComparable
func TisComparable[P T]() {
_ = isComparable[P]
}
func isComparable[_ comparable]() {}
どういうことなのでしょうか?実はそもそも型パラメータ型のspec-comparabilityは現在の言語仕様上明確に定義が書いてないのですが、これは次のように修正されるだろうと書かれています。
Type parameters are comparable if each type in the type parameter's type set implements comparable.
型パラメータは、その型セットに属するそれぞれの型が
comparableを実装するときにcomparableである。
これを踏まえてもう一度コードをみてみます。
// define a helper function with a type parameter P constrained by T
// and use that type parameter with isComparable
func TisComparable[P T]() {
_ = isComparable[P]
}
func isComparable[_ comparable]() {}
- Proposal採用後のGo言語では
Pがcomparableを満たすのはPがspec-comparableなときでした。 - 型パラメータ型である
Pがspec-comparableになるのはPの型制約であるTの型集合に属するすべての型がcomparableを実装するときです。 -
Tは普通の非インタフェース型なのでTの型集合はTのみからなる集合です。 - その集合の唯一の要素である
Tがcomparableを実装するということは、Tはstrictly comparableです。 - よって、このコードのコンパイルができるならば
Tはspec-comparableであるばかりかstrictly comparableでもあるということが保証できます。
まとめ
以上をまとめると、つぎのVenn図のようになるとおもわれます:
- spec-comparableはstrictly comparableよりも広い
- spec-comparableな範囲は"satisfy comparable"な範囲と一致する
- strictly comparableな範囲は"implement comparable"な範囲と一致する

Discussion