メインコンテンツへスキップ
#69230likely_accept

golang.org/x/exp/collection/set: new package with a generic set type

ステータス変更: active likely_accept

要約

AIによる要約であり、誤りを含む場合があります。

概要

集合(set)を表すジェネリック型 Set[E] を、実験用パッケージ golang.org/x/exp/collection/set に追加する提案です。中身は map[E]struct{} そのもので、Go APIで集合を受け渡すときの標準的な型として試験的に導入されます。

ステータス変更

active → likely_accept
2026年10月7日に @jba が最終版のAPIを提示しました。その翌日にactiveとなり、10月29日に @aclements がproposal review groupを代表してlikely acceptを宣言しています。同日の指摘はドキュメントの書き方に関するものだけでした。「All の順序は "likely random" ではなく非決定的と書くべき」「String の並び順は fmt を引用するだけでなく具体的に書くべき」という2点で、API設計そのものへの反対はありませんでした。この提案は、2025年末に発足したCollections working group(@jba、@adonovan、@griesemer、@mvdan、@rogpeppe、@khr、@ianlancetaylor)がまとめたコレクション関連提案(#80590)の一部です。グループ内で設計がすでに練られていたことが、短期間で合意に至った理由と考えられます。

技術的背景

現状の問題点

標準ライブラリには集合型がなく、map[T]bool か map[T]struct{} で代用するのが慣習です。和集合や積集合のような基本操作も、毎回 make とループで手書きする必要があります。パッケージごとに表現が異なるため、公開APIで集合を受け渡す共通の型もありません。

提案された解決策

type Set[E comparable] map[E]struct{}
func Of[E comparable](elems ...E) Set[E]
func Collect[E comparable](seq iter.Seq[E]) Set[E]

メソッドは次のとおりです。

  • 基本操作: All, Clear, Clone, Contains, Delete, DeleteFunc, Equal, Insert, Intersects, Len, String
  • 新しい集合を返す集合演算: Union, Intersection, Difference, SymmetricDifference
  • レシーバ自身を書き換える集合演算: UnionWith, IntersectionWith, DifferenceWith, SymmetricDifferenceWith
    Insert と Delete は、要素数が変わったかどうかを bool で返します。
    型定義が map そのものなので、len(s)、_, ok := s[e]、for e := range s がそのまま使え、map[E]struct{} とも相互に代入できます。Len・Contains・All は組み込み操作と重複しますが、抽象的な集合を受け取るジェネリック関数に Set を渡せるようにするために用意されています。

これによって何ができるようになるか

  • 2つのスライスに共通要素があるかを、O(n*m) の二重ループを使わずに簡潔に判定できる
  • 公開APIで「文字列の集合」などを受け渡すときに、共通の型として使える
  • 既存の map[E]struct{} を変換コストなしで集合として扱える

コード例

// Before: map と手書きループで積集合を作る
a := map[string]struct{}{"x": {}, "y": {}}
b := map[string]struct{}{"y": {}, "z": {}}
inter := make(map[string]struct{})
for k := range a {
	if _, ok := b[k]; ok {
		inter[k] = struct{}{}
	}
}
// After
x := set.Of("x", "y")
y := set.Collect(slices.Values([]string{"y", "z"}))
z := x.Intersection(y)   // {y}
fmt.Println(z)           // "{y}"
for e := range z {       // map と同じく range で回せる
	println(e)
}

議論のハイライト

  • 当初案(2024年)と @ianlancetaylor の案は、map を内部に隠す struct 型でした。この方式では値レシーバとポインタレシーバのどちらにするかが問題になりました。最終的にworking groupが、map をそのまま型にする「透過的」な定義を採用しました。参照型としての振る舞いが map と同じなので直感的だ、というのが理由です。
  • ContainsFunc は却下されました。All() をループで回すのと計算量が変わらないためです。一方 DeleteFunc は、走査と削除をまとめて効率化できるので採用されました。InsertIf(bool, E) の要望もありましたが、外部のヘルパーで簡単に書けるとして採用されていません。
  • 置き場所は container/set → golang.org/x/container/sets → x/exp/collection/set と変遷しました。その途中で ContainsAll、InsertAll、DeleteAll が削除され、一時的に SetOf/CollectSet と改名された関数名は Of/Collect に戻されました。
  • 比較できない要素型(comparable でない型)向けのハッシュベースの集合は #80584 として、既存の map 型に集合操作を提供するヘルパー関数は #77052 として、別の提案に分けられています。
  • @jimmyfrasche は、String が利用している fmt の map ソートロジックを公開して、サードパーティのコレクションでも使えるようにすべきだと提起しました。

関連リンク