The Go Blog

さようならコア型 — こんにちは、私たちが知って愛するGo!

Robert Griesemer
2025年3月26日

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

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

型パラメータと型制約

型パラメータは、将来の型引数のプレースホルダーです。コンパイル時に値がわかる型変数のように機能し、名前付き定数がコンパイル時に値がわかる数値、文字列、またはブール値を表すのと似ています。通常の変数と同様に、型パラメータにも型があります。その型は、それぞれの型パラメータの型であるオペランドで許可される操作を決定する型制約によって記述されます。

型パラメータをインスタンス化する具体的な型は、型パラメータの制約を満たす必要があります。これにより、型パラメータの型であるオペランドは、型パラメータのインスタンス化に使用される具体的な型が何であっても、それぞれの型制約のすべてのプロパティを確実に持ちます。

Goでは、型制約はメソッドと型の要件の組み合わせによって記述され、これらが合わせて型セットを定義します。これは、すべての要件を満たすすべての型のセットです。Goは、この目的のために汎用化された形式のインターフェースを使用します。インターフェースはメソッドと型のセットを列挙し、そのようなインターフェースによって記述される型セットは、それらのメソッドを実装し、列挙された型に含まれるすべての型で構成されます。

例えば、インターフェースによって記述される型セットは

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

その表現が[]byteまたはstringであり、メソッドセットにHashメソッドが含まれるすべての型で構成されます。

これにより、ジェネリックオペランドに対する操作を規定するルールを書き出すことができます。例えば、インデックス式のルールでは(とりわけ)型パラメータ型Pのオペランドaに対して

インデックス式a[x]は、Pの型セット内のすべての型の値に対して有効でなければなりません。Pの型セット内のすべての型の要素型は同一でなければなりません。(この文脈では、文字列型の要素型はbyteです。)

これらのルールにより、以下のジェネリック変数sのインデックス付けが可能になります(playground

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

インデックス操作s[i]は、sの型がbytestringであり、bytestringの型制約(型セット)に[]bytestring型が含まれており、これらはiによるインデックス付けが有効であるため許可されます。

コア型

この型セットに基づいたアプローチは非常に柔軟で、元のジェネリクス提案の意図と合致しています。ジェネリック型オペランドを含む操作は、それぞれの型制約によって許可される任意の型に対して有効であれば、有効であるべきです。実装に関して物事を簡素化し、後でルールを緩和できることを知っていたため、このアプローチは普遍的に選択されませんでした。代わりに、例えば送信ステートメントの場合、仕様には次のように記載されています。

チャネル式のコア型はチャネルでなければならず、チャネル方向は送信操作を許可しなければならず、送信される値の型はチャネルの要素型に割り当て可能でなければなりません。

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

  • 型が型パラメータでない場合、そのコア型は単純にその基底型です。
  • 型が型パラメータである場合、コア型は型パラメータの型セット内のすべての型の単一の基底型です。型セットに異なる基底型がある場合、コア型は存在しません。

例えば、interface{ ~[]int }にはコア型([]int)がありますが、上記のConstraintインターフェースにはコア型がありません。さらに複雑なことに、チャネル操作や特定の組み込み呼び出し(appendcopy)に関しては、上記のコア型の定義は制限的すぎます。実際のルールには、異なるチャネル方向や[]bytestring型の両方を含む型セットを許可するための調整があります。

このアプローチには様々な問題があります。

  • コア型の定義は、異なる言語機能に対して健全な型規則を導く必要があるため、特定の操作に対して過度に制限的です。例えば、Go 1.24のスライス式に関するルールはコア型に依存しており、その結果、Constraintによって制約された型Sのオペランドのスライスは、有効である可能性があるにもかかわらず許可されません。

  • 特定の言語機能を理解しようとするとき、非ジェネリックコードを検討する場合でも、コア型の複雑さを学ぶ必要があるかもしれません。再びスライス式の場合、言語仕様はスライスされたオペランドのコア型について言及しており、オペランドが配列、スライス、または文字列でなければならないと述べているだけではありません。後者の方がより直接的で、単純で、明確であり、具体的なケースでは関係ない可能性のある別の概念を知る必要がありません。

  • コア型の概念が存在するため、インデックス式、およびlencap(その他)に関するルールは、すべてコア型を避けており、言語において規範ではなく例外のように見えます。その結果、コア型は、x.fセレクタがxの型セットのすべての要素によって共有されるフィールドfにアクセスすることを許可するissue #48522のような提案を、言語にさらに例外を追加するように見せかけてしまいます。コア型がなければ、その機能は非ジェネリックフィールドアクセスに関する通常のルールの自然で有用な結果となります。

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プログラムには影響しませんが、将来の言語改善への道を開くと同時に、現在の言語を学びやすくし、その仕様を簡素化します。

次の記事: testing.B.Loopによるより予測可能なベンチマーク
前の記事: トラバーサル耐性のあるファイルAPI
ブログインデックス