#80584active
golang.org/x/exp/collection/hashed: Set, a set based on a hash table that uses the element type's custom hash function and equivalence relation
新規提案
要約
AIによる要約であり、誤りを含む場合があります。
概要
比較可能でない型や、== とは別の等価判定を使いたい型の値を、集合(Set)として扱うための型 hashed.Set[E] を golang.org/x/exp/collection/hashed に追加する提案です。ハッシュ関数と等価関係を利用者が指定でき、和集合・積集合などの集合演算メソッドも備えます。
ステータス変更
(新規) → active
2026年10月8日、@aclements がこのproposalを proposal review meeting の active 列に追加し、毎週のレビュー対象になりました。Collections working group は Go のコレクション型をまとめて検討しており(傘となるissueは #80590)、これはその一環です。同じパッケージに入る hashed.Map(#69559)の Set 版にあたります。前日の10月7日には @jba が API を確定版に近い形に整理しており、実装CL(go.dev/cl/845508)もすでに出ています。レビューに進める材料がそろったため active になったと考えられます。
技術的背景
現状の問題点
Go の組み込み map は、キーが comparable(== で比較できる型)であることを前提にしています。そのため、次のような値は集合として扱えません。
- スライスを含む構造体など、比較できない型の値
- 大文字小文字を区別しない文字列や、
go/typesの型のように「構造が同じなら等しい」とみなしたい値
こうした値を扱うには、正規化したキーを作ってmapに入れるか、独自にハッシュテーブルを実装する必要がありました。
提案された解決策
最新版(2026年10月7日)の API は次のとおりです。
package hashed // golang.org/x/exp/collection/hashed
type Set[E any] struct { /* unexported */ }
func NewSet[E any](hasher maphash.Hasher[E]) *Set[E]
// 基本操作
All, Clear, Clone, Contains, Delete, DeleteFunc, Insert, Len, String, Equal
// 集合演算(新しい集合を返す版と、レシーバを書き換える ...With 版)
Union/UnionWith, Intersection/IntersectionWith, Intersects,
Difference/DifferenceWith, SymmetricDifference/SymmetricDifferenceWith
主な仕様は以下のとおりです。
NewSetで作る必要があります。nil ポインタやゼロ値は有効な集合ではありません。コピーはせず、Cloneを使います。- 二項演算(
Unionなど)を行う2つの集合は、同じ等価関係を使っていなければなりません。 - NaN を含む浮動小数点の要素は使えません。実装が
e == eという前提で処理を省略することがあるためです(例: 空でないsに対してIntersects(s, s)は要素を調べずに true を返します)。 Stringは要素を、順序は規定しないものの毎回同じ順序で出力します。Allで反復している途中で集合を変更しても安全です。- 比較可能な型を通常の
==で扱うなら、組み込みのmapかcollection/setを使うよう明記されています。
これによって何ができるようになるか
- 型解析ツール:
go/typesの型を「同一の型」として重複なく管理できます(types.Typeは==では構造の同一性を判定できません)。 - 独自の等価判定: 大文字小文字を区別しない文字列の集合や、正規化した座標の集合などを作れます。
- 比較できない複合値: スライスを含む構造体を、中身に基づいて重複排除できます。
コード例
// Before: 正規化したキーを map に入れるワークアラウンド
seen := map[string][]int{}
for _, v := range values { // v は []int
seen[fmt.Sprint(v)] = v // 文字列化のコストがかかり、等価の定義もあいまい
}
// After: hashed.Set に独自の Hasher を渡す
type sliceHasher struct{}
func (sliceHasher) Hash(h *maphash.Hash, s []int) {
for _, x := range s { maphash.WriteComparable(h, x) }
}
func (sliceHasher) Equal(a, b []int) bool { return slices.Equal(a, b) }
s := hashed.NewSet[[]int](sliceHasher{})
for _, v := range values {
s.Insert(v)
}
other := hashed.NewSet[[]int](sliceHasher{})
common := s.Intersection(other) // 同じ等価関係なので集合演算ができる
議論のハイライト
- 既存の Set と統合する案の却下: @willfaught は「
container/set.Setに Hasher をオプションで渡せばよいのでは」と提案しました。@adonovan は、set.Setは改訂により組み込み map をそのまま土台にした型になっており、独自の等価関係が必要な場面だけhashed.Setを使う、と役割分担を説明しました。等価関係を型ごとに切り替えたい場合はmaphash.ComparableHasherを使えます。 - 置き場所の変更: 当初は標準ライブラリの
container/hashとして提案されましたが、x/exp/container/sets.Hashedを経て、最終的にx/exp/collection/hashed.Setになりました。まず実験的なモジュールで試す方針です。 - API の削減:
ContainsAll、InsertAll、DeleteAll(イテレータを受け取る一括操作)は削除されました。 - 未解決の懸念: @seankhliao は
DifferenceやSymmetricDifferenceという名前が分かりにくいと指摘しました。@prattmic は、このままだと JSON に空オブジェクトとしてシリアライズされてしまうため、カスタムマーシャラーを用意すべきではないかと問題提起しています。どちらも今後のレビューで議論される可能性があります。