Go言語のジェネリクス入門
Go1.18は2022年3月にリリースされました。このリリースはGo言語へのジェネリクスの実装を含んでいます。
この記事ではできるだけ最新の仕様と用語法にもとづいてジェネリクスの言語仕様について解説していきます。
更新履歴
- 2024/01/03: Go1.21(2023-08-08)で
cmp
パッケージが標準ライブラリに追加されたことに対応しました。 - 2023/02/23: Go1.20(2023-02-01)の
comparable
の仕様変更に対応しました。- 次の関連資料があります:
- The Go Blog - All your comparable types Griesemer氏によるGo公式ブログです。
- Go言語のBasic Interfaceはcomparableを満たすようになる(でも実装するようにはならない) 上記の内容に対する筆者の解説記事です。Go1.20リリース前に書いたので用語が使えてないところがあります。
- 次の関連資料があります:
シリーズ
タイトル | 内容 |
---|---|
Go言語のジェネリクス入門(1) | この記事です。基本的なジェネリクスの使用法とunion, ~について説明します。 |
Go言語のジェネリクス入門(2) インスタンス化と型推論 | この記事の続編です。インスタンス化と型推論、そこで使われるunificationというルーチンについてできるだけ厳密に説明します。 |
章目次
実用上は最初の「基本原則とシンプルな例」というセクションの内容で十分なことが多いと思います。とりあえずここだけ読むのをおすすめします。
タイトル | 内容 |
---|---|
基本原則とシンプルな例 | Goジェネリクスの基本原則と典型的な例を解説します。 |
unions |
メソッドの実装以外の性質をジェネリックに扱いたい場合に使える機能を解説します。 |
~ とunderlying type |
unions をさらに使いこなすための文法~ を解説します。 |
core type | 言語仕様書読み込み勢(?)向けです。続編の前提知識になります。 |
基本原則とシンプルな例
Goジェネリクスの基本原則とシンプルな例を説明します。シンプルな例と言っても、ユースケースの大半はこれで尽くされると思いますので、この節だけ読んで終わりにするのもおすすめです。
Goのジェネリクスの基本原則
Goのジェネリクスの基本事項についてはType Parameters Proposalの冒頭に挙げられています。このうち特に重要なのは次の2つです。この2つを覚えればGoのジェネリクスを十分に使うことができると思います。
- 「関数」と「型」は「型パラメータ」を持つことができる。
- 「型パラメータ」の満たすべき性質は「インタフェース型」を「型制約」として使うことで表す。
f[T Stringer]
具体例1: 型パラメータを持つ関数まず「型パラメータを持つ関数」の具体例を見てみましょう。
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
を受け取る、という意味になります。
Stack[T any]
具体例2: 型パラメータを持つ型関数だけでなく、型も「型パラメータ」を持つことができます。
一例として、データ構造「スタック」を実装してみます。
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はメソッドPush
とPop
を持ちます。型パラメータを持つ型に対してメソッドを宣言するときは、次のような構文を使います。
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] {
// ...
}
Set[T comparable]
具体例3: 型パラメータを持つ型次に、いわゆるSet型を定義してみましょう。
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
ではダメなのでしょうか?
それは、T
をmap
の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を受け付ける
このようなコードは書けません。MyInt
はStringer
を満たすので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
のことをunions
やunion 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
でループを回せるという性質をみてみましょう。
面白みのない例ですが、次のコードはコンパイルできます。
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つ挙げておきます。
「ある名前のフィールドを持つ」という性質を型制約で表現することはできません。
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
}
では、次のように定義したNewInt
やNewNewInt
に対してMax
関数を使用できるでしょうか?
type NewInt int
type NewNewInt NewInt
「できない」というのが答えです。int, NewInt, NewNewInt
はそれぞれ相異なる型であり、したがってNewInt
とNewNewInt
はNumber
インタフェースを実装しないからです。
~
をつかってunderlying typeをマッチングする
NewInt
やNewNewInt
も数値型であることに変わりはなく、>=
などの演算子で比較することができるのですから、このような型を許すインタフェースを作りたいです。
もちろん、NewInt
を直接unionsに加えればNewInt
にNumber
を実装させることはできます:
// 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
自身である - それ以外の場合、(
T
はtype T X
のように定義された型なので)T
のunderlying typeはX
のunderlying typeである
のように再帰的な定義になっています。
より丁寧な解説を見たいかたは、DQNEOさんによる次の発表を見るのが良いと思います。
cmp
パッケージ
<, >
で順序づけできる型をunions
で列挙できることは分かりましたが、実際に全ての型を書こうとすると面倒だなと思われた方もいると思います。
そこで、<, >
で順序づけできる型によって満たされる型制約は標準パッケージcmp
のcmp.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.
//
// Note that floating-point types may contain NaN ("not-a-number") values.
// An operator such as == or < will always report false when
// comparing a NaN value with any other value, NaN or not.
// See the [Compare] function for a consistent way to compare NaN values.
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
これを使って、一般的なMax
関数を定義できます。
package main
import (
"cmp"
"fmt"
)
type NewInt int
var x, y NewInt = 3, 2
func main() {
fmt.Println(Max(x, y)) // 3
}
func Max[T cmp.Ordered](x, y T) T {
if y > x {
return y
}
return x
}
unions
の要素としてどんな型でも書いていいのか
細かい話になりますが、unions
の要素として使える型については、少し制約があります。
unions
が複数要素からなるとき、その要素になれるのは
- 非インタフェース型
- メソッド定義を含まないインタフェース型
です。つまり、fmt.Stringer
のようにString() string
というメソッド定義を含んでいる型は複数要素からなるunions
の要素とすることができません。
まとめ
-
~
をつかうと型定義によって作りうる無限の型にインタフェースを実装させることができる -
~T
はT
をunderlying typeに持つすべての型を表す
core type
underlying typeを一般化した新しい概念であるcore typeと、それがどのように仕様に関わるかについて説明します。
定義
定義はこちらにあるのですが、
- この定義どおりに読むと型セット(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 E
がT
のcore typeである - 受信チャネル
<-chan E
か送信チャネルchan<- E
のどちらか一方のみがunderlying typeに含まれていれば、それがT
のcore typeである
- underlying 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が関係しています。
例えば次のコードは動作します。
type C interface { // structural type = struct { Field int }
struct{ Field int }
}
func F[T C](T) {
_ = T{Field: 1} // composite literalを作れる
}
しかし次のコードは動作しません。
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を持っていないといけません。
例えば次の例は動作します。
// 動作する例
type I interface {
[]int
}
func f[T I](x T) {
for range x {
}
}
しかし次の例は(どちらもスライスであるにもかかわらず)動作しません。
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から得られた理解が本質的でした。いつもありがとうございます。
Discussion