👋

Go言語のBasic Interfaceはcomparableを満たすようになる(でも実装するようにはならない)

2022/11/18に公開約7,100字

前回の記事Go言語のcomparableには3つの意味があるにおいて、言語仕様のcomparableと型制約のcomparableは指す範囲が異なるということを説明しました。たとえば、any型は言語仕様上comparableですが、comparable型制約を満たしていません。

このギャップをなくすProposalがacceptされそうです。今回はその内容を説明します。

https://github.com/golang/go/issues/56548

言語仕様としての理屈にそれほど関心がない読者の人は要約だけ読めば十分だと思います。

要約

  • unionsをふくまないinterface型(basic interface)について、Go1.18時点ではcomparable型制約を満たさないようになっていた
  • このproposalが実装されると、basic interfaceはcomparable型制約を満たすことができるようになる
  • 特別な場合として、anycomparable型制約を満たすようになるので、次のようなコードが書けるようになる(今は書けない)

https://go.dev/play/p/_TyieBbyzXx

func f[T comparable](T) {}

func main() {
	var x any
	f(x)
}

用語の整理

authorのgriesemerさんは議論のために次の用語を用いています。

用語 前回の記事における対応する用語 意味
spec-comparable comparable(言語仕様) 言語仕様上==を使ってもコンパイルができるすべての型
strictly comparable comparable(型制約) ==panicせずに比較できる型

前回の記事のvenn図とは次のように対応します:

comparableの種類

proposalで何が変わるのか

現在の状態(Go1.18)

Go1.18では、あらゆる型制約Cについて、

  • TCを実装する(implement)
  • TCを満たす(satisfy)
  • TCの型集合に属する

という3つの文章は全く同じ意味です。

そしてcomparableの型集合は、strictly comparableな型全体からなる集合です。つまり、

  • spec-comparableな非インタフェース型
  • strictry-comparableな型のみをフィールドに持つstruct型

だけがcomparableの型集合に属します。これを示すのが次のサンプルコードです。

https://go.dev/play/p/ULeOmhRP6s3

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採用後は次のようになります。

  • TCを実装する(implement)
  • TCの型集合に属する

の2つの文章は全く同じ意味です。この点はGo1.18と変わりありません。

しかし、

  • TCを満たす(satisfy)

はこの2つよりも広い場合にあてはまることがあります。

また、先程のサンプルコードはすべてコンパイルできるようになります(執筆時点ではできません)

https://go.dev/play/p/ULeOmhRP6s3

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採用後でもコンパイルできません。

https://go.dev/play/p/vTpk2lXlA6L

func f[T comparable](T) {}

func main() {
	var s []int
	f(s) // compile error: sliceはspec-comparableですらない
}

型制約を満たす(satisfy)の定義

  • TCを実装する(implement)
  • TCを満たす(satisfy)

この2つは異なる意味を持つようになると書きました。では、型TCを満たす(satisfy)とは正確にはどのような意味になるのでしょうか?

proposalによると、型Tが型制約Cを満たす(satisfy)のは次の2つのいずれかのときです。

  • TCを実装する(implement)とき
  • Cinterface{comparable; E}の形で書けて、Tがspec-comparableであり、かつTEを実装する(implement)とき
    • ここで、Eはbasic interfaceつまりunionsを含まないinterfaceであるものとする

具体例を見る前に、なぜこの2つを別概念にする必要があるかを説明します。

なぜimplementとsatisfyを別概念にする必要があるか

要請として、anycomparable型制約を満たす(satisfy)ようにしなければいけないとしましょう。
その上でsatisfyとimplementが全く同じ意味であると仮定すると、次のようにまずいことになります。

守るべき前提として、[]intのようにspec-comparableではない型がcomparable型制約を満たさないようにしなければいけません。
ここで、[]intanyを実装することに注意すると、次のようなことが言えます。

  • []intanyを実装する (anyの定義から当然)
  • anycomparableを実装する (要請と、satisfyとimplementの同義性から)
  • []intcomparableを実装しない (守るべき前提)

すると、[]intanyを実装し、anycomparableを実装するのに、[]intcomparableを実装しないという奇妙な結論になってしまいます。
つまり、implementするという関係は本来推移法則を満たさないといけないはずなのに、推移法則を満たさない結果になってしまっています。

これは言語仕様として困るので、ここまでで使った前提のどれかは諦めないといけません。
最も諦めがつくのは、implementとsatisfyが同義であるという前提です。

具体例

anycomparableを満たす

proposalによると、型Tが型制約Cを満たす(satisfy)のは次の2つのいずれかのときです。

  • TCを実装する(implement)とき
  • Cinterface{comparable; E}の形で書けて、Tがspec-comparableであり、かつTEを実装する(implement)とき
    • ここで、Eはbasic interfaceつまりunionsを含まないinterfaceであるものとする

Ccomparableを代入して整理すると次のようになります。comparableはbasic intefaceであるanyをつかって次のように書けることに気をつけます:

// これは疑似コードです
type comparable inteface {
	comparable 
	any
}

Tが型制約comparableを満たす(satisfy)のは次の2つのいずれかのときである:

  • Tcomparableを実装する(implement)とき
  • Tがspec-comparableであり、かつTanyを実装する(implement)とき

どんな型もanyを実装することを使えば、この条件は次と同じことです:

Tが型制約comparableを満たす(satisfy)のは次の2つのいずれかのときである:

  • Tcomparableを実装する(implement)とき
  • Tがspec-comparableであるとき

この2つのうち前者は後者に含まれますから、結局、

Tが型制約comparableを満たす(satisfy)のはTがspec-comparableであるときである

と簡単にできます。

Tanyを代入すればanyはspec-comparableなので、anycomparableを満たします。

より複雑な制約

type C interface {
	comparable
	String() string
}

fmt.Stringerがこれを実装するかを考えてみましょう。fmt.Stringercomparableを実装しません。しかし、fmt.Stringerはspec-comparableであり、かつString() stringを実装します。したがって fmt.StringerCを実装はしませんがCを満たします。

[]intはどうでしょうか?[]intはspec-comparableではないのでCを満たしません。

ある型がstrictly comparableであることをコンパイル時にチェックする方法

Go1.18ではstrictly comparableな型だけがcomparableを満たしていたので、panicせずに==, !=で比較できる型であることをコンパイル時にチェックできました。

proposal採用後は、ある型Tpanicせずに==, !=で比較できる型であるのをコンパイル時にチェックできなくなるのでしょうか?

これにはauthorのgriesemerさんが次のような方法を提示しています。

https://github.com/golang/go/issues/56548#issuecomment-1317673963

// 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は現在の言語仕様上明確に定義が書いてないのですが、これは次のように修正されるだろうと書かれています。

https://github.com/golang/go/issues/56548#issuecomment-1319052631

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言語ではPcomparable満たすのはPがspec-comparableなときでした。
  • 型パラメータ型であるPがspec-comparableになるのはPの型制約であるTの型集合に属するすべての型がcomparable実装するときです。
  • Tは普通の非インタフェース型なのでTの型集合はTのみからなる集合です。
  • その集合の唯一の要素であるTcomparable実装するということは、Tstrictly comparableです。
  • よって、このコードのコンパイルができるならばTはspec-comparableであるばかりかstrictly comparableでもあるということが保証できます。

まとめ

以上をまとめると、つぎのVenn図のようになるとおもわれます:

  • spec-comparableはstrictly comparableよりも広い
  • spec-comparableな範囲は"satisfy comparable"な範囲と一致する
  • strictly comparableな範囲は"implement comparable"な範囲と一致する

comparableの種類

GitHubで編集を提案

Discussion

ログインするとコメントできます