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