Slide 1

Slide 1 text

Go 1.24 でジェネリックになった型エイリアスの紹介 syumai Go 1.24 Release Party (2025/2/26)

Slide 2

Slide 2 text

自己紹介 syumai ECMAScript 仕様輪読会 / Asakusa.go 主催 株式会社ベースマキナで管理画面のSaaS を開発中 Go でGraphQL サーバー (gqlgen) や TypeScript でフロント エンドを書いています Software Design 2023 年12 月号から2025 年2 月号まで Cloudflare Workers の連載をしました Twitter ( 現𝕏): @__syumai Website: https://syum.ai

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

ジェネリックになった型エイリアス

Slide 5

Slide 5 text

ジェネリックになった型エイリアス 従来、型エイリアスは型パラメータを持つことができなかった Go 1.24 で、型エイリアスに型パラメータを持てるようになり、従来は不可能だった表 現が可能に 特に、ジェネリック型の互換性を維持したリファクタという観点で重要

Slide 6

Slide 6 text

言語仕様の変更点

Slide 7

Slide 7 text

従来の仕様 従来、型パラメータを持つことができたのは、型定義 (type definition) と 関数宣言 (function declaration) のみ 型定義によって導入される、型パラメータを持つ型のことを、 ジェネリック型 (generic type) と呼ぶ 型パラメータは、以下のように、 型制約 (type constraints) を伴って宣言される // 型定義の例 type Map[K comparable, V any] map[K]V // 関数宣言の例 func Min[T cmp.Ordered](a, b T) T { if a < b { return a } return b }

Slide 8

Slide 8 text

ジェネリックエイリアス Go 1.24 から、エイリアス宣言 (alias declaration) も型パラメータを持つことができる ようになった 型パラメータを持つ型エイリアスのことを、 ジェネリックエイリアス (generic alias) と呼ぶ // エイリアス宣言の例 type Map[K comparable, V any] = map[K]V

Slide 9

Slide 9 text

ジェネリックエイリアス ジェネリックエイリアスの型パラメータは、複合型 (composite type) の一部や、別の ジェネリックな型への型引数として使える // 複合型の一部 type IntMap[K comparable] = map[K]int // 別のジェネリックな型への型引数 // iter package の `type Seq2[K, V any] func(yield func(K, V) bool)` に対するエイリアス type IndexedSeq[V any] = iter.Seq2[int, V]

Slide 10

Slide 10 text

ジェネリックエイリアス ジェネリックエイリアスは、使用する際に必ずインスタンス化 (instantiation) される必 要がある type Map[K comparable, V any] = map[K]V // NG var m1 Map // 型引数を省略することはできない // OK var m2 Map[string, int]

Slide 11

Slide 11 text

ジェネリックエイリアス ジェネリックエイリアスの型パラメータそのものに対するエイリアス宣言を行うこと はできない type A[P any] = P // illegal: P is a type parameter

Slide 12

Slide 12 text

ジェネリックエイリアス ジェネリックエイリアスを無効化したい場合は、 GOEXPERIMENT=noaliastypeparams のフラグを設定できる Go 1.25 でこのフラグはなくなる見込み

Slide 13

Slide 13 text

ジェネリック型と、ジェネリックエイリアスの違い

Slide 14

Slide 14 text

ジェネリック型と、ジェネリックエイリアスの違い 型同一性 (type identity) の点で異なる 型定義によって導入されたジェネリック型は、使用する際に必ずなんらかの名前付き 型 (named type) を得る ある名前付き型は常に他の名前付き型とは異なる型となる( 型同一性の定義の一部) type IntMap[K comparable] map[K]int m1 := map[string]int{"a": 1} // m1 はmap[string]int 型 m2 := IntMap[string]{"b": 2} // m2 はIntMap[string] 型

Slide 15

Slide 15 text

ジェネリック型と、ジェネリックエイリアスの違い ジェネリックエイリアスの機能そのものによって、新たに名前付き型が導入されるこ とはない 複合型のリテラル型に対するエイリアスだった場合は、名前付き型が導入されな い ジェネリック型に対するエイリアスだった場合は、エイリアスの対象の型をもと にした名前付き型が導入される type IntMap[K comparable] = map[K]int m1 := map[string]int{"a": 1} // m1 はmap[string]int 型 m2 := IntMap[string]{"b": 2} // m2 もmap[string]int 型

Slide 16

Slide 16 text

主なユースケース

Slide 17

Slide 17 text

主なユースケース 型エイリアスの最も主要なユースケースは、プログラムの リファクタリング 特に、パッケージの移動などの大規模なリファクタリング 2024 年9 月17 日にGo Blog に書かれた型エイリアスについての記事でも、この点が強調 されている

Slide 18

Slide 18 text

型エイリアス導入の背景 そもそも、型エイリアスはどのようなモチベーションがあって導入されたのか? その背景説明として、パッケージのリファクタを行う例を示します

Slide 19

Slide 19 text

型エイリアス導入の背景 あるパッケージ p の分割について考える パッケージ p には、以下の関数 F 、定数 C 、型 T が存在する package p func F() T { return 1 } const C = 1 type T int

Slide 20

Slide 20 text

型エイリアス導入の背景 パッケージ p を使うコードの例 package main import "github.com/syumai/example/p" func main() { // OK: p.F() はp.T 型、p.C は型無しの定数 if p.F() == p.C { println("p.F() == p.C") } var v p.T = 1 // OK: p.F() はp.T 型、v もp.T 型 if p.F() == v { println("p.F() == v") } ... }

Slide 21

Slide 21 text

型エイリアス導入の背景 パッケージ分割を行い、関数 F を p1 に、 定数 C を p2 に、型 T を p3 に移動 ここで、後方互換性を維持し、パッケージ p を使用しているコードが引き続き利用可 能な状態に保つ方法を考える package p1 import "github.com/syumai/example/p3" func F() p3.T { return 1 } package p2 const C = 1 package p3 type T int

Slide 22

Slide 22 text

型エイリアス導入の背景 関数は、移管先のパッケージの関数を呼ぶ形でラップすればOK 定数は、移管先のパッケージの定数を単に参照すればOK package p // OK func F() p3.T { return p1.F() } // OK const C = p2.C

Slide 23

Slide 23 text

型エイリアス導入の背景 型も、パッケージ p 側に type T p3.T として改めて定義すればよいように見えるが NG p.T と p3.T は異なる型 type T p3.T の型定義により新たな名前付き型が導入されているため package p // p.T とp3.T は別の型 type T p3.T

Slide 24

Slide 24 text

型エイリアス導入の背景 パッケージ p を利用するコードで p3.T を期待するコードのビルドに失敗する package main import "github.com/syumai/example/p" func main() { ... var v p.T = 1 if p.F() == v { // build error (F の返すp3.T とp.T は異なる型) println("p.F() == v") } ... }

Slide 25

Slide 25 text

型エイリアス導入の背景 Go 1.9 で導入された型エイリアスを使うと、パッケージ p で新たな名前付き型が導 入されない p.T と p3.T は同じ型になるため、後方互換性が保たれる package p // p.T という名前付き型は導入されない type T = p3.T → 型エイリアスは、型定義をパッケージを跨いで移動するリファクタを可能にした

Slide 26

Slide 26 text

ジェネリック型のリファクタリング 従来の型エイリアスによるリファクタリングはジェネリック型に対してはできなかっ た 型エイリアスが型パラメータを持てなかったため

Slide 27

Slide 27 text

ジェネリック型のリファクタリング 先ほどの例で、パッケージ p の型 T がジェネリック型だったとする package p type T[P any] struct { Field P } 型T をパッケージ p からパッケージ p3 に移動したとき、パッケージ p 側で p3.T にエイリアス宣言する際に問題が発生する package p3 // パッケージp から移動 type T[P any] struct { Field P } package p // NG type T = p3.T

Slide 28

Slide 28 text

ジェネリック型のリファクタリング ジェネリック型は、使用する際に型引数を指定して、インスタンス化する必要がある 下記のエイリアス宣言 ( 前ページから引用) では p3.T の型パラメータ P に渡す 型引数が定まらない エイリアス指定先と同じ型パラメータ宣言を暗黙的に行う仕様はない package p // NG type T = p3.T // p3.T には型引数が必要! // type T[P any] = p3.T[P] ← のように暗黙的に宣言してくれない

Slide 29

Slide 29 text

ジェネリック型のリファクタリング Go 1.24 のジェネリックエイリアスで、初めてジェネリック型の定義をパッケージを跨 いで移動するリファクタが可能になった package p // OK type T[P any] = p3.T[P]

Slide 30

Slide 30 text

その他のユースケース 型パラメータ追加時の後方互換性維持 型パラメータを事前に指定したジェネリック型の公開 複合型の型名の省略

Slide 31

Slide 31 text

型パラメータ追加時の後方互換性維持 ジェネリックエイリアスは、ジェネリック型に型パラメータが増えた際の後方互換性 維持に使える 例: string 型の値をキーとして任意の V 型の値を保持する Cache 型 type Cache[V any] struct{ m sync.Map } func (c *Cache[V]) Put(key string, v V) { /* */ } func (c *Cache[V]) Get(key string) V { /* */ }

Slide 32

Slide 32 text

型パラメータ追加時の後方互換性維持 Cache 型は、以下のようにインスタンス化して使う type User struct {} var userCache Cache[User]

Slide 33

Slide 33 text

型パラメータ追加時の後方互換性維持 Cache 型のキーを任意の型に差し替え可能にしようとすると、後方互換性が崩れる → 型名と、型パラメータの数・制約の両方を維持する必要がある type Cache[K comparable, V any] struct{/* */} var userCache Cache[User] // 型引数の数が合わない

Slide 34

Slide 34 text

型パラメータ追加時の後方互換性維持 別の名前の型 CacheKV に実装を移し、 Cache はキー型を string に固定した型エ イリアスとすることで解決 type CacheKV[K comparable, V any] struct{ m sync.Map } func (c *Cache[K, V]) Put(key K, v V) { /* */ } func (c *Cache[K, V]) Get(key K) V { /* */ } // 後方互換性を保ったまま新しい型に移行完了! type Cache[V any] = CacheKV[string, V]

Slide 35

Slide 35 text

型パラメータを事前に指定したジェネリック型の公開 ジェネリック型の型引数を事前に指定した形で公開可能 上記の後方互換性維持もこのユースケースの一例 ライブラリだとユースケースがあるかも type ( intMap[K comparable] map[K]int StringIntMap = intMap[string] IntIntMap = intMap[int] )

Slide 36

Slide 36 text

複合型の型名の省略 長い型名を単に省略する → 従来のジェネリック型の定義では、新たな名前付き型が導入されてしまうの で、このユースケースではエイリアスの方が適切 type Proxy[In, Out any] = func(ctx context.Context, in In) (Out, error) // generic alias あり func registerProxy1[In, Out any](p Proxy[In, Out]) {} // generic alias なし func registerProxy2[In, Out any](p func(ctx context.Context, in In) (Out, error)) {}

Slide 37

Slide 37 text

まとめ 型エイリアスの主なユースケースはプログラムのリファクタリング 特に、型定義のパッケージを跨いだ移動での後方互換性の維持 ジェネリックエイリアスにより、ジェネリック型の定義を別パッケージに移動しやす くなった ジェネリクスの導入ハードルがまた一つ下がった!

Slide 38

Slide 38 text

ご清聴ありがとうございました!

Slide 39

Slide 39 text

おまけ

Slide 40

Slide 40 text

導入に時間がかかった理由 Go 1.18 でのジェネリクス導入時に合わせてリリースするべきかの議論が行われていた が、十分にジェネリクスが使われるようになり、その知見が溜まってから実装した方 がよいというGo team の判断によって初期スコープから外されていた https://github.com/golang/go/issues/46477#issuecomment-852701491 Go 1.18 のリリースからほぼ3 年が経過、十分に使われるようになったため、このタイ ミングでの実装になったのではないでしょうか