😸

Go言語のジェネリクス入門(1)

2022/02/18に公開約20,500字

Go1.18のリリースは2022年3月の予定になっており、ジェネリクスを含む言語仕様書はかなりの頻度で加筆修正されています。
この記事ではできるだけ最新の仕様と用語法にもとづいてジェネリクスの言語仕様について解説していきます。

シリーズ

タイトル 内容
Go言語のジェネリクス入門(1) この記事です。基本的なジェネリクスの使用法とunion, ~について説明します。
Go言語のジェネリクス入門(2) インスタンス化と型推論 この記事の続編です。インスタンス化と型推論、そこで使われるunificationというルーチンについてできるだけ厳密に説明します。

章目次

実用上は最初の「基本原則とシンプルな例」というセクションの内容で十分なことが多いと思います。とりあえずここだけ読むのをおすすめします。

タイトル 内容
基本原則とシンプルな例 Goジェネリクスの基本原則と典型的な例を解説します。
unions メソッドの実装以外の性質をジェネリックに扱いたい場合に使える機能を解説します。
~とunderlying type unionsをさらに使いこなすための文法~を解説します。
core type 言語仕様書読み込み勢(?)向けです。続編の前提知識になります。

基本原則とシンプルな例

Goジェネリクスの基本原則とシンプルな例を説明します。シンプルな例と言っても、ユースケースの大半はこれで尽くされると思いますので、この節だけ読んで終わりにするのもおすすめです。

Goのジェネリクスの基本原則

Goのジェネリクスの基本事項についてはType Parameters Proposalの冒頭に挙げられています。このうち特に重要なのは次の2つです。この2つを覚えればGoのジェネリクスを十分に使うことができると思います。

  • 「関数」と「型」は「型パラメータ」を持つことができる。
  • 「型パラメータ」の満たすべき性質は「インタフェース型」を「型制約」として使うことで表す。

具体例1: 型パラメータを持つ関数f[T Stringer]

まず「型パラメータを持つ関数」の具体例を見てみましょう。

https://gotipplay.golang.org/p/NWxONCa85DL
func main() {
	fmt.Println(f([]MyInt{1, 2, 3, 4}))
    // Output:
    // [1 2 3 4]
}

// fは型パラメータを持つ関数
// Tは型パラメータ
// インタフェースStringerは、Tに対する型制約として使われている
func f[T Stringer](xs []T) []string {
	var result []string
	for _, x := range xs {
        // xは型制約StringerによりString()メソッドが使える
		result = append(result, x.String())
	}
	return result
}

type Stringer interface {
	String() string
}

type MyInt int

// MyIntはStringerを実装する
func (i MyInt) String() string {
	return strconv.Itoa(int(i))
}

関数fの宣言時にf[T Stringer]という四角カッコの文法要素がついていますね。これが型パラメータと一緒に導入される新しい文法です。この意味は、

  • 関数fにおいて型パラメータTを宣言する
  • T型はインタフェースStringerを実装する型である、という型制約を設ける

という意味です。このように宣言した型パラメータは関数の他の部分で参照できます。例えば引数の型としてTを使うことができます。よって、f[T Stringer](xs []T)というのは、引数xsとして型Tのスライス型[]Tを受け取る、という意味になります。

具体例2: 型パラメータを持つ型Stack[T any]

関数だけでなく、型も「型パラメータ」を持つことができます。

一例として、データ構造「スタック」を実装してみます。

https://gotipplay.golang.org/p/jCS7vhCe_XC
type Stack[T any] []T

func New[T any]() *Stack[T] {
	v := make(Stack[T], 0)
	return &v
}

func (s *Stack[T]) Push(x T) {
	(*s) = append((*s), x)
}

func (s *Stack[T]) Pop() T {
	v := (*s)[len(*s)-1]
	(*s) = (*s)[:len(*s)-1]
	return v
}

func main() {
	s := New[string]()
	s.Push("hello")
	s.Push("world")
	fmt.Println(s.Pop()) // world
	fmt.Println(s.Pop()) // hello
}

まず型定義において、type Stack[T any] []Tとしています。もしstringに限定したスタックであればtype Stack []stringと定義するところです。このstringの部分をパラメータ化するために[T any]を追加したわけです。

ここで、anyは新しく導入される識別子で、空インタフェースinterface{}の別名です。anyを書けるところには代わりにinterface{}を書いても構いませんし、その逆もOKです。Stackの内容になる要素型は何の型であっても良いですから、anyを型制約にするのが適切です。

次にコンストラクタであるNew関数を見てみます。型自体がパラメータ化されているので、コンストラクタも型パラメータを持つ関数としています。

StackはメソッドPushPopを持ちます。型パラメータを持つ型に対してメソッドを宣言するときは、次のような構文を使います。

func(s *Stack[T]) Push(x T)

*とポインタにしてあるのはポインタレシーバにするためで、これは従来通りの文法です。少し覚えにくいのはレシーバの型をStack[T]のようにして型パラメータをつける必要があるところです。このTをメソッド内の別な場所で参照することができます。Pushの場合は引数の型として(x T)と使っていますね。

最後にmainを見てみましょう。

	s := New[string]()

という行がありますね。関数宣言ではなく、関数呼び出しの方に[string]がついています。これは型引数(type argument)で、型パラメータに具体的な型を渡すための構文です。

型引数は通常は省略可能です。それは、関数の引数の型と型パラメータをマッチングさせて型パラメータを型推論できる場合が多いからです。しかし、New関数には引数がないため、具体的な型引数を渡さないと型推論ができず、コンパイルが失敗します。

メソッド宣言において新たな型パラメータは宣言できない

メソッドも関数の一種なので、メソッド宣言時に新たな型パラメータを宣言できるのかという疑問が湧くかもしれません。これはできないことになっています。

例えば次のようなコードは書けません。

// これは書けない
func (s *Stack[T]) ZipWith[S,U any](x *Stack[S], func(T, S) U) *Stack[U] {
    // ...
}

こういうことをしたければメソッドではない関数として定義すべきです。

// これは書ける
func ZipWith[S,T,U any](x *Stack[T], y *Stack[S], func(T, S) U) *Stack[U] {
    // ...
}

具体例3: 型パラメータを持つ型Set[T comparable]

次に、いわゆるSet型を定義してみましょう。

https://gotipplay.golang.org/p/ht_akn1eCGy
type Set[T comparable] map[T]struct{}

func New[T comparable](xs ...T) Set[T] {
	s := make(Set[T])
	for _, xs := range xs {
		s.Add(xs)
	}
	return s
}

func (s Set[T]) Add(x T) {
	s[x] = struct{}{}
}

func (s Set[T]) Includes(x T) bool {
	_, ok := s[x]
	return ok
}

func (s Set[T]) Remove(x T) {
	delete(s, x)
}

func main() {
	s := New(1, 2, 3)
	s.Add(5)
	fmt.Println(s.Includes(3)) // true
	s.Remove(3)
	fmt.Println(s.Includes(3)) // false
}

型定義に注目してください。

type Set[T comparable] map[T]struct{}

ここで、comparableという新しいインタフェース型が型制約に使われています。なぜanyではダメなのでしょうか?

それは、Tmapのkeyとして使いたいからです。mapはkeyの値に重複がないように値を保管していくデータ構造なので、重複しているかどうかを判定できる必要があります。その判定には==及び!=演算子による比較が用いられます。Go言語ではこの2つの演算子により比較できる型と比較できない型があるため、「比較可能なすべての型により実装されるインタフェース」が必要なのです。

しかしそのようなインタフェースをユーザが定義することはできません。そこでGo言語はcomparableというインタフェースを予め定義されたものとして提供することにしました。これを使えば、genericに使えるSet型を簡単に作ることができます。

Go1.17でできなかったこと

ここで型パラメータのモチベーションを知るために、Go1.17でのコードを考えてみます。

インタフェース型のスライスを受け取る関数

まず、Go1.17において次のインタフェースと関数を考えます。

type Stringer interface {
    String() string
}

func f(xs []Stringer) []string {
    var result []string
    for _, x := range xs {
        result = append(result, x.String())
    }
    return result
}

また、次のようにStringerを実装する型を用意します。

type MyInt int

// MyIntはStringerを実装する
func(i MyInt) String() string {
    return strconv.Itoa(int(i))
}

このとき次のように、MyIntのスライスをfに渡すことはできるでしょうか?

xs := []MyInt{0,1,2}
f(xs) // fは[]Stringerを受け付ける

このようなコードは書けません。MyIntStringerを実装するのでMyInt型の値はStringer型の変数に代入可能ですが、[]MyInt型の値は[]Stringer型の変数に代入できないためです。

Go1.17で[]Stringerを一般的に扱う関数を書くには、次のf2のように空インタフェース型interface{}を受け取るようにするしかありませんでした。この関数f2にはどんな型の値でも渡せてしまうので、関数の利用側で間違った値を渡さないように気をつけなければいけません。

// 【注意】 Stringerを実装する型Tのスライス[]Tだけを渡すこと
func f2(xs interface{}) {
    if vs,ok := xs.([]MyInt); ok {
        // vsに関する処理
    }
    // ... 
}

型パラメータによる記述

Stringerインタフェースを実装する型Tのスライス[]Tだけを渡すこと

という条件付けは、型パラメータを使うと次のように記述できます。

// fは型パラメータを持つ関数
// Tは型パラメータ
// インタフェースStringerは、Tに対する型制約として使われている
func f[T Stringer](xs []T) []string {
	var result []string
	for _, x := range xs {
        // xは型制約StringerによりString()メソッドが使える
		result = append(result, x.String())
	}
	return result
}

このfには[]MyInt型の値だけでなく、何のコードの変更もなしにStringerを実装する型Tのスライス[]Tを渡せます。また、そうでない型の値を渡した場合はコンパイルエラーとして検出できます。

まとめ

  • 「関数」と「型」は「型パラメータ」を持つことができる。
  • 「型パラメータ」の満たすべき性質は「インタフェース型」を「型制約」として使うことで表す。
  • 関数や型の後に[T constraint]という文法要素をつけると、「型パラメータTを宣言する。Tはconstraintを実装しなければならない」という意味になる。constraintは型制約と呼ばれ、インタフェース型を用いる。
  • メソッドに追加で型パラメータを宣言することはできない。
  • anyインタフェースは空インタフェースinterface{}の別名である
  • 比較可能、つまり==, !=による等値判定が可能な非インタフェース型により実装されるcomparableが提供される。
  • 型パラメータの重要な使い方の1つは、スライスやマップなどのいわゆるコレクション型の抽象化である。

unions

Go1.18ではunionsという文法要素を使って、従来のインタフェースでは表現できない型制約を定義することができます。

genericなMax関数とunions

標準パッケージのmath.Max関数はfunc Max(x, y float64) float64というシグネチャを持ち、float64の値しか渡すことができません。

せっかく型パラメータが使えるようになるので、genericなMax関数を作ってみたいと思います。まず初めに次のようなコードを考えました。

func Max(T any) (x, y T) T {
	if x >= y {
		return x
	}
	return y
}

ところが、このコードは動作しません。実行すると次のエラーメッセージが出力されます。Tの型制約はanyなので、演算子>=で比較できるとは限らないからです。

invalid operation: cannot compare x >= y (operator >= not defined on T)

無効な演算: x >= yという比較はできません。(演算子 >= は 型Tで定義されていません)

それでは、適当なインタフェース型を定義して演算子>=で比較できるような型制約にすることはできるでしょうか?

Go1.17までは、できませんでした。なぜなら、Go1.17までのインタフェース型とは「メソッドセット」すなわちメソッドの集合(集まり)を定義するものであって、「ある演算子が使える」というようなメソッド以外の型の性質を表すことはできないからです。

そこでGo言語は、「インタフェース型」として次のようなものも定義できるように機能を拡張することにしました。

type Number interface {
    int | int32 | int64 | float32 | float64
}

このNumberというインタフェースは、int, int32, int64, float32, float64という5種類の型によって 「実装」 され、これ以外の型によっては実装されません。
この文法要素int | int32 | int64 | float32 | float64のことをunionsunion elementと呼びます。

大切なことは、Numberを実装する全ての型は、演算子>=をサポートしていることです。これにより、次のような関数を書くことができます。

type Number interface {
    int | int32 | int64 | float32 | float64
}

func Max[T Number] (x, y T) T {
	if x >= y {
		return x
	}
	return y
}

for~rangeが使えるインタフェース型

型の満たす性質にはいくつか種類があります。ここでいくつか挙げてみましょう。

  • あるメソッドを持っているという性質
  • ==, !=で比較できるという性質
  • <, >, >=, <=で順序づけられるという性質
  • for ~ range文でループを回すことができるという性質
  • ある名前のフィールドを持っているという性質
  • etc

「あるメソッドを持っているという性質」は従来のインタフェース型で表現できます。==, !=で比較可能な性質は、組み込みのcomparableインタフェースで表現できるのでしたね。そして<, >などで順序づけられる性質はunionsを利用した新しいインタフェースで表現できることをみました。

次に、for ~ rangeでループを回せるという性質をみてみましょう。

面白みのない例ですが、次のコードはコンパイルできます。

https://gotipplay.golang.org/p/ec6KpsOHgHv
type I interface {
	[]int 
}

func f[T I](x T) {
	for range x {
	}
}

Iを実装する型は[]intのみで、かつこの型はfor rangeでループすることができる型です。このような場合、Iを型制約とする型パラメータの値に対してfor rangeループを書くことができます。

unionsを含むインタフェースは型制約でしか使えない

型制約ではなく通常の変数の型としてunionsを使うと、いわゆるsum typeのようなものが定義できそうに見えます。しかし、現在のところこれは許可されません。

type IntString interface {
	int | string
}

var x IntString // これはできない

つまり、unionsを使ったインタフェース型は型制約としてしか使えず、通常の変数の型としては使えません。

この制限は将来的に取り除かれる可能性があります。型パラメータの導入だけでも非常に大きな変更であるため、安全を期するためにまずは最低限の機能でリリースし、実際の使われ方からフィードバックを得て判断していくのだと思います。

フィールドを持つという性質は型制約で扱えない

できそうでできないことを1つ挙げておきます。

「ある名前のフィールドを持つ」という性質を型制約で表現することはできません。

https://gotipplay.golang.org/p/WEM-yelirK1
type I interface {
	X
}

type X struct {
	SomeField int
}

func f[T I](x T) {
	fmt.Println(x.X) // これはできない
}

まとめ

  • Goの型パラメータは型制約をインタフェース型によって表現するが、型の性質には「メソッドを持つ」以外の性質もある。その性質の一部はunionsを利用した新しいインタフェース型によって表現できる。
  • <, >, <=, >=による順序付可能性はunionsを使って順序づけられる型のみを列挙することで表現できる。
  • for rangeループができる型を使って、その1つの型だけからなるunionsによる型制約を作ると、その型制約に従う型パラメータ型の値についてfor rangeループができる。
  • ==, !=による比較可能性はcomparableインタフェースで表現する(再掲)。
  • フィールドを持つという性質を型制約で表して、型パラメータ型の値のフィールドを参照することはできない。

~とunderlying type

モチベーション

前章ではunionsを使ったインタフェース定義により、複数の数値型に適用できるgenericなMax関数を作れることを見ました。

type Number interface {
    int | int32 | int64 | float32 | float64
}

func Max[T Number] (x, y T) T {
	if x >= y {
		return x
	}
	return y
}

では、次のように定義したNewIntNewNewIntに対してMax関数を使用できるでしょうか?

type NewInt int

type NewNewInt NewInt

「できない」というのが答えです。int, NewInt, NewNewIntはそれぞれ相異なる型であり、したがってNewIntNewNewIntNumberインタフェースを実装しないからです。

~をつかってunderlying typeをマッチングする

NewIntNewNewIntも数値型であることに変わりはなく、>=などの演算子で比較することができるのですから、このような型を許すインタフェースを作りたいです。

もちろん、NewIntを直接unionsに加えればNewIntNumberを実装させることはできます:

// NewIntとNewNewIntがNumberを実装するようになった
type Number interface { 
    int | int32 | int64 | float32 | float64 | NewInt | NewNewInt
}

しかし、「intを元にして型定義で作られる新しい型」は無限にあるので、それら全てがNumberを実装するようにしたいです。そのための文法として、Go言語は~を導入しました。

type Number interface { 
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

このように定義すると、「int, int32, int64, float32, float64のうちいずれかをunderlying typeとする型」すべてがNumberを実装するようになります。

これにより、次のようなコードが書けるようになります。

type Number interface { 
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

func Max[T Number] (x, y T) T {
	if x >= y {
		return x
	}
	return y
}

var x y NewInt = 1, 2

max := Max(x, y) // max == NewInt(2)

underlying type

Go言語の全ての型は、それに対応する"underlying type"という型を持っています。

1つの型に対して、対応するunderlying typeは必ず1つだけ存在します。underlying typeを持たない型や、underlying typeを2つ以上持つ型は存在しません。

具体例

まず具体例を見てみます。

type NewInt int // NewIntのunderlying typeはint

type NewNewInt NewInt // NewNewIntのunderlying typeもint

// intのunderlying typeはint

type IntSlice []int // IntSliceのunderlying typeは[]int

// []intのunderlying typeは[]int

大まかにいうと、type A Bという形の型定義を左から右に遡ってゆき、それ以上遡れないところにある型がunderlying typeです。

厳密な定義(Go 1.17)

https://go.dev/ref/spec##Types によると、

Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its type declaration.

とあります。つまり、

  • Tが事前宣言されたboolean, 数値, 文字列型や型リテラルのとき、Tのunderlying typeはT自身である
  • それ以外の場合、(Ttype T Xのように定義された型なので)Tのunderlying typeはXのunderlying typeである

のように再帰的な定義になっています。

より丁寧な解説を見たいかたは、DQNEOさんによる次の発表を見るのが良いと思います。

constraintsパッケージとunions

<, >で比較可能な型をunionsで列挙できることは分かりましたが、実際に全ての型を書こうとすると面倒だなと思われた方もいると思います。

そこで、順序付けできるとか、数値型である、などの基本的な型制約はパッケージconstraintsで提供されることになりました。

https://github.com/golang/exp/blob/master/constraints/constraints.go

例えは、順序付できる型を表すインタフェースはconstraints.Orderedです:

// Ordered is a constraint that permits any ordered type: any type
// that supports the operators < <= >= >.
// If future releases of Go add new ordered types,
// this constraint will be modified to include them.
type Ordered interface {
	Integer | Float | ~string
}

Integer, Floatも同じconstraingsパッケージで定義されているインタフェースです。

ここでunionsの要素として別なインタフェース型が初めて出てきましたね。

Integerunionsの一部として使った場合どういう意味になるかというと、Integerunionsに列挙されている型をIntegerの代わりに書いたのと同じ意味になります。Floatも同様です。
従って、Orderedは次のように書いても同じ意味です。

type Ordered interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 | 
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
	~float32 | ~float64 | ~string
}

これを使って、一般的なMax関数を定義できます。

https://gotipplay.golang.org/p/fBQ5QUeLFb3
type NewInt int

var x, y NewInt = 3, 2

func main() {
	fmt.Println(Max(x, y)) // 3
}

func Max[T constraints.Ordered](x, y T) T {
	if y > x {
		return y
	}
	return x
}

unionsの要素としてどんな型でも書いていいのか

unionsが複数要素からなるとき、その要素になれるのは

  • 非インタフェース型
  • メソッド定義を含まないインタフェース型

です。つまり、fmt.StringerのようにString() stringというメソッド定義を含んでいる型は複数要素からなるunionsの要素とすることができません。

まとめ

  • ~をつかうと型定義によって作りうる無限の型にインタフェースを実装させることができる
  • ~TTをunderlying typeに持つすべての型を表す

core type

underlying typeを一般化した新しい概念であるcore typeと、それがどのように仕様に関わるかについて説明します。

定義

https://tip.golang.org/ref/spec#Core_types

定義はこちらにあるのですが、

  • この定義どおりに読むと型セット(type set)の概念が必要なので型セットを使わずに読み替えたい
  • 型パラメータ型のcore typeについて言語仕様書の記述に疑問がある

ことから、筆者が妥当と考えている定義を以下記載します。言語仕様書がアップデートされ次第こちらもできるだけメンテするつもりです。

core typeが存在する場合としない場合

Go言語の型はcore typeという型を持つ場合と持たない場合があります。型Tがcore typeを持つのは、次の場合です。

  • Tがインタフェース型でも型パラメータでもないとき
  • Tがインタフェース型であり、つぎのいずれかに該当するとき
    • Tを実装する全ての型のunderlying typeUが同一であるとき
    • Tを実装する全ての型は、同一の要素型Eのチャネル型であり、かつ、それらが方向付きチャネルを含む場合にはその方向が同一であるとき
      • つまり、Eの受信チャネル<-chan Eと送信チャネルchan<- Eの両方は含んでいないとき
  • Tが型パラメータであり、その型制約(常にインタフェース型)がcore typeをもつとき

これ以外のすべての場合、Tはcore typeを持ちません。

Tがcore typeをもつ場合、それぞれのケースにおいてcore typeは次のように決まります。

  • Tがinterface型でも型パラメータでもないとき、Tのcore typeはTのunderlying typeである
  • Tがinterface型のとき
    • そのTを実装する全ての型のunderlying typeUが同一であるとき、Tのcore typeはUである
    • そのTを実装する全ての型のunderlying typeが同一の要素型Eを持つchannel型であり、
      • underlying typeが双方向チャネル型のみであれば、その双方向チャネル型chan ETのcore typeである
      • 受信チャネル<-chan Eか送信チャネルchan<- Eのどちらか一方のみがunderlying typeに含まれていれば、それがTのcore typeである
  • Tが型パラメータ型で、Tの型制約がcore typeを持つとき、それがTのcore typeである

channelの場合には例外的な規定が必要なのでややこしくなっていますが、大雑把に言えば次のような理解で大丈夫です。

大雑把なcore typeの理解

  • Tがインタフェース型でも型パラメータでもないとき、Tのcore typeはTのunderlying type
  • Tがインタフェース型であり、Tを実装する全ての型のunderlying typeUが同一であればTはcore typeを持ち、Tのcore typeはU
  • Tがインタフェース型であり、Tを実装する全ての型のunderlying typeUが同一でなければTはcore typeを持たない(これはchannelの例外があり厳密には正しくない)
  • Tが型パラメータであるとき、Tのcore typeはTの型制約のcore type(core typeが存在するかしないか含めて型制約に従う)

具体例

次の型制約はcore typeをもつでしょうか?またその場合core typeは何でしょうか?

type C1 interface {
	~[]int
}

type C2 interface {
	int | string
}
  • C1を実装する全ての型のunderlying typeは[]intなのでC1はcore typeをもち、core typeは[]intです。
  • C2を実装する型はint, stringなのでunderlying typeは同一でなくC2はcore typeをもちません。

core typeの登場場面

このcore typeですが、次のような場面で登場します。

  • composite literals
  • for range
  • 制約型推論(後述)

本章では最初の2つについて説明します。

composite literals

型パラメータ型を使って、composite literalsを書くことができる場合があります。
その条件にもcore typeが関係しています。

例えば次のコードは動作します。

https://gotipplay.golang.org/p/RFvZrv_hp6T
type C interface { // structural type = struct { Field int }
	struct{ Field int }
}

func F[T C](T) {
	_ = T{Field: 1} // composite literalを作れる
}

しかし次のコードは動作しません。

https://gotipplay.golang.org/p/7UY0hO2-rlj
type C interface {
	struct{ Field int } | struct { Field int `tag` }
}

func F[T C](T) {
	_ = T{Field: 1}
}

composite literalを作るのに使う型名が型パラメータ型の名前である場合、その制約はcore typeを持っていなければいけません。
2つ目の例は、全く同じ構造のstruct型のunionsであるにもかかわらず、struct tagの有無によって型の同一性が満たされず、Cのcore typeが存在しないため、Tのcomposite literalは作れません。

for rangeループ

前章for rangeループについて扱いましたが、実は型パラメータ型に対してfor rangeループを回すためには、制約がcore typeを持っていないといけません。

例えば次の例は動作します。

https://gotipplay.golang.org/p/ec6KpsOHgHv
// 動作する例
type I interface {
	[]int 
}

func f[T I](x T) {
	for range x {
	}
}

しかし次の例は(どちらもスライスであるにもかかわらず)動作しません。

https://gotipplay.golang.org/p/biQ_41vglso
type I interface {
	[]int | []string
}

func f[T I](x T) {
	for range x {
	}
}

./prog.go:11:12: cannot range over x (variable of type T constrained by I) (T has no core type)

制約型推論とcore type

他にcore typeの関連仕様として欠かせないのは型推論アルゴリズムの一部である「制約型推論」です。
「制約型推論」が適用されるためには、型制約がcore typeをもつことが必要となっています。これを理解することで、「なんでこれは型推論できるのにこれはできないの?」という疑問にスッキリ答えられるようになるでしょう。

型推論については、続編で解説できればと思います。

まとめ

  • core typeはunderlying typeを拡張したような概念である
  • 「for~rangeループで使える」などの型の性質を型制約で表現したい場合、型制約がcore typeを持つことが必要となる
  • core typeは制約型推論という型推論アルゴリズムにも使われる(詳細は続編で)

最後に

この記事をかくにあたり#gospecreadingから得られた理解が本質的でした。いつもありがとうございます。

GitHubで編集を提案

Discussion

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