メインコンテンツへスキップ
#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 に空オブジェクトとしてシリアライズされてしまうため、カスタムマーシャラーを用意すべきではないかと問題提起しています。どちらも今後のレビューで議論される可能性があります。

関連リンク