さようならコア型、ようこそ本来のGoへ

Goodbye core types - Hello Go as we know and love it! by Robert Griesemer

Go 1.18のリリースではジェネリクスが導入され、それに伴い型パラメータ、型制約、型集合といった新しい概念を含む数多くの新機能がもたらされました。 このリリースでは、コア型 という概念も導入されました。 前者が具体的な新機能を提供するものだったのに対し、コア型は便宜上、そしてジェネリックなオペランド(型が型パラメータであるオペランド)を扱いやすくするために導入された抽象的な構成要素です。 Goコンパイラでは、これまでオペランドの基底型に依存していたコードが、代わりにそのオペランドのコア型を計算する関数を呼び出す必要が出てきました。 言語仕様では、多くの箇所で「基底型」を「コア型」に置き換えるだけで済みました。 何も問題はないように思えるでしょう?

ところが、実際にはかなりの問題がありました!ここに至った経緯を理解するために、まずは型パラメータと型制約がどのように機能するかを簡単に振り返っておくと役立ちます。

型パラメータと型制約

型パラメータとは、将来与えられる型引数のためのプレースホルダーです。名前付き定数がコンパイル時に値が判明している数値、文字列、真偽値を表すのと同じように、型パラメータはコンパイル時に値が判明している 型変数 のように振る舞います。通常の変数と同様に、型パラメータにも型があります。その型は 型制約 によって記述され、型制約はそのオペランドの型が該当の型パラメータであるときにどのような操作が許されるかを決定します。

型パラメータをインスタンス化する具象型は、その型パラメータの制約を満たさなければなりません。これにより、型パラメータをインスタンス化するのにどの具象型が使われたとしても、型が型パラメータであるオペランドは、対応する型制約が持つすべての性質を備えていることが保証されます。

Goでは、型制約はメソッドの要件と型の要件を組み合わせて記述され、それらが合わさって 型集合 を定義します。型集合とは、すべての要件を満たす型全体の集合のことです。Goはこの目的のために一般化されたインターフェースを使います。インターフェースはメソッドと型の集合を列挙するものであり、そのようなインターフェースによって記述される型集合は、列挙されたメソッドを実装し、かつ列挙された型に含まれるすべての型からなります。

例えば、次のインターフェースによって記述される型集合を考えてみましょう。

type Constraint interface {
    ~[]byte | ~string
    Hash() uint64
}

この型集合は、表現が []byte または string であり、かつメソッド集合に Hash メソッドを含むすべての型からなります。

ここまでの内容によって、ジェネリックなオペランドに対する操作を規定するルールを記述できるようになりました。例えば、インデックス式のルールでは(他の規定に加えて)、型パラメータ型 P を持つオペランド a について次のように定められています。

インデックス式 a[x] は、P の型集合に含まれるすべての型の値に対して有効でなければならない。P の型集合に含まれるすべての型の要素型は同一でなければならない。(この文脈では、string型の要素型はbyteである。)

これらのルールによって、次のジェネリックな変数 s をインデックスできるようになります(プレイグラウンド):

func at[bytestring Constraint](s bytestring, i int) byte {
    return s[i]
}

インデックス操作 s[i] が許されているのは、s の型が bytestring であり、bytestring の型制約(型集合)には i によるインデックス操作が有効な []byte 型と string 型が含まれているからです。

コア型

この型集合に基づくアプローチは非常に柔軟であり、当初のジェネリクス提案の意図とも合致しています。すなわち、ジェネリックな型のオペランドが関与する操作は、その型制約によって許容されるどの型に対しても有効であれば、有効であるべきだという考え方です。しかし実装面を簡単にするため、そして後からルールを緩められるとわかっていたことから、このアプローチはあらゆる場面で採用されたわけではありませんでした。例えばSend文については、仕様は次のように定めています。

チャネル式の コア型 はチャネルでなければならず、チャネルの方向は送信操作を許すものでなければならず、送信される値の型はチャネルの要素型に代入可能でなければならない。

これらのルールは、おおよそ次のように定義される「コア型」という概念に基づいています。

  • 型が型パラメータでない場合、そのコア型は単にその基底型である。
  • 型が型パラメータである場合、コア型は、その型パラメータの型集合に含まれるすべての型に共通する単一の基底型である。型集合に含まれる型の基底型が異なる場合、コア型は存在しない。

例えば、interface{ ~[]int } にはコア型([]int)がありますが、先ほどの Constraint インターフェースにはコア型がありません。話をさらにややこしくしているのが、チャネル操作や一部の組み込み関数呼び出し(appendcopy)に関しては、上記のコア型の定義では制限が厳しすぎるという点です。実際のルールには、チャネルの方向が異なる場合や、型集合に []bytestring の両方が含まれる場合を許容する調整が加えられています。

このアプローチには、さまざまな問題がありました。

  • コア型の定義は、言語のさまざまな機能に対して健全な型付けのルールをもたらすものでなければならないため、特定の操作にとっては過度に制限的になってしまいます。例えば、Go 1.24のスライス式のルールはコア型に依存しており、その結果、Constraint によって制約された型 S のオペランドをスライスすることは、本来なら有効であってもよいはずなのに、許されていません。

  • 特定の言語機能を理解しようとするとき、ジェネリクスを使わないコードを考えている場合でさえ、コア型の込み入った仕組みを学ばなければならないことがあります。ここでもスライス式を例に取ると、言語仕様は、オペランドが配列、スライス、文字列のいずれかでなければならないと単純に述べる代わりに、スライスされるオペランドのコア型について語っています。後者の言い方のほうが直接的で、単純で、明快であり、具体的な場面では無関係かもしれない別の概念を知っておく必要がありません。

  • コア型という概念が存在するせいで、コア型を避けているインデックス式や lencap(その他)のルールは、言語の中で標準ではなくむしろ例外であるかのように見えてしまいます。さらに、コア型があることで、issue #48522のような提案(セレクタ x.f によって、x の型集合に属するすべての要素が共有するフィールド f にアクセスできるようにする提案)が、言語にさらなる例外を追加するものであるかのように見えてしまいます。コア型がなければ、この機能は、ジェネリクスを使わない通常のフィールドアクセスのルールから自然かつ有用に導かれる結果になります。

Go 1.25

来たるGo 1.25リリース(2025年8月)に向けて、私たちは言語仕様からコア型という概念を取り除き、必要な箇所には明示的な(そして意味的に等価な!)説明文を用いることに決めました。これには複数の利点があります。

  • Go仕様が扱う概念が減り、言語を学びやすくなる。
  • ジェネリクスを使わないコードの振る舞いを、ジェネリクスの概念を参照せずに理解できるようになる。
  • 個別対応のアプローチ(特定の操作に対する特定のルール)によって、より柔軟なルールへの道が開ける。すでに触れたissue #48522に加え、より強力なスライス操作や、型推論の改善についてのアイデアもある。

対応する提案であるissue #70128は最近承認され、関連する変更はすでに実装されています。具体的には、言語仕様の多くの説明文が、ジェネリクス導入以前の元の形に戻され、ジェネリックなオペランドに関するルールを説明するために必要な箇所には新しい段落が追加されました。しかも、動作は一切変わっていません。コア型に関する節は丸ごと削除されました。コンパイラのエラーメッセージも「コア型」に言及しないように更新され、多くの場合、型集合の中のどの型が問題を引き起こしているのかをより具体的に指摘するようになりました。

実際にどのような変更が行われたか、一例を見てみましょう。組み込み関数 close について、Go 1.18以降、仕様は次のように始まっていました。

コア型がチャネルである引数 ch に対して、組み込み関数 close は、これ以上そのチャネルに値が送信されないことを記録する。

単に close がどう動作するかを知りたいだけの読者も、まずコア型について学ばなければなりませんでした。Go 1.25以降、この節は再びGo 1.18より前と同じ書き出しに戻ります。

チャネル ch に対して、組み込み関数 close(ch) は、これ以上そのチャネルに値が送信されないことを記録する。

こちらのほうが短く、理解しやすくなっています。読者がジェネリックなオペランドを扱う場合に限って、新しく追加された次の段落を考慮する必要があります。

close の引数の型が型パラメータである場合、その型集合に含まれるすべての型は、同一の要素型を持つチャネルでなければならない。それらのチャネルのいずれかが受信専用チャネルである場合はエラーとなる。

コア型に言及していた他の箇所についても、同様の変更を行いました。まとめると、今回の仕様の更新は既存のいかなるGoプログラムにも影響を与えませんが、将来の言語改善への道を開きつつ、現時点での言語をより学びやすく、仕様をより単純なものにしています。

By Robert Griesemer