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

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

ステータス変更: active likely_accept

要約

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

概要

キーの比較方法を利用者が決められるハッシュテーブル型の集合 Set[E] を、新しいパッケージ golang.org/x/exp/collection/hashed に追加する提案です。要素の型が == で比較できない場合や、== とは違う「同じとみなす基準」を使いたい場合に使います。

ステータス変更

active → likely_accept
2026-10-08 にレビュー対象(active)になり、2026-10-28 の Proposal Review Meeting で likely accept と判断されました。同じ会議では、コレクション全体をまとめる提案 #80590 と、関連する #69559(hashed.Map)、#69230(collection/set)、#60630(collection/sorted)もそろって likely accept になっています。この提案は、Collections working group が進めるコレクション型をまとめて整備する作業の一部と位置づけられます。議論の中で配置先とAPIが絞り込まれ、大きな反対意見は残りませんでした。@aclements からはドキュメントの細かな指摘(Nit)が1件あっただけです。

技術的背景

現状の問題点

Go の組み込み map で集合を表す場合、要素の型は comparable(== で比較できる型)でなければならず、等しいかどうかの判定にも == しか使えません。そのため、次のような集合は書けません。

  • スライスを含む構造体や go/types.Type など、== で比較できない、または == では意味的な等しさを判定できない値の集合
  • 大文字小文字を区別しない文字列の集合のように、独自の等価関係(どの値同士を同じとみなすかのルール)を使う集合
    これまでは、要素を正規化した文字列などに変換して map[string]T のキーにするといった回避策を取る必要がありました。

提案された解決策

maphash.Hasher[E](ハッシュ関数 Hash と等価判定 Equal を組にしたインターフェース)をコンストラクタに渡して Set を作ります。最新のAPIは次のとおりです。

package hashed // golang.org/x/exp/collection/hashed
func NewSet[E any](hasher maphash.Hasher[E]) *Set[E]
// 基本操作: All, Clear, Clone, Contains, Delete, DeleteFunc, Insert, Len, String, Equal
// 集合演算: Union / Intersection / Difference / SymmetricDifference(新しい集合を返す)
//           UnionWith / IntersectionWith / DifferenceWith / SymmetricDifferenceWith(s 自体を書き換える)
//           Intersects

仕様上の主な制約は次のとおりです。

  • NewSet で作る必要があります。ゼロ値や nil は使えず、値のコピーも禁止です(複製は Clone を使います)。
  • comparable な型を通常の == で扱うなら、組み込み map か collection/set を使うよう案内しています。
  • NaN を含む浮動小数点数は避けるよう求めています。e == e が常に成り立つ前提で処理を省略する場合があるためです(例: 空でない s について Intersects(s, s) は要素を見ずに true を返します)。
  • 二項演算に渡す2つの集合は、同じ等価関係を使っていなければなりません。

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

  • 型チェッカーやツール: go/types.Type のように構造的に等しいかで比べたい値の集合を扱えます。
  • 独自の正規化: 大文字小文字を無視する文字列、末尾の / を無視するパスなど、独自の等価基準による重複排除ができます。
  • 比較できない複合値: スライスを含む構造体などの集合を、内容のハッシュに基づいて扱えます。
  • 和集合や差集合といった集合演算がメソッドとして用意されているため、自分でループを書く必要がなくなります。

コード例

// Before: 正規化したキーを使う map で代用する
seen := map[string]string{} // 正規化したキー → 元の値
for _, s := range names {
    k := strings.ToLower(s)
    if _, ok := seen[k]; !ok {
        seen[k] = s
    }
}
// After: 等価関係を持つ Hasher を渡して Set を作る
type foldHasher struct{}
func (foldHasher) Hash(h *maphash.Hash, s string) { h.WriteString(strings.ToLower(s)) }
func (foldHasher) Equal(a, b string) bool        { return strings.EqualFold(a, b) }
set := hashed.NewSet[string](foldHasher{})
for _, s := range names {
    set.Insert(s) // "Go" と "GO" は同じ要素として扱われる
}
other := hashed.NewSet[string](foldHasher{})
other.Insert("go")
fmt.Println(set.Intersects(other), set.Union(other).Len())

(Hash と Equal は、Equal が true になる2つの値に必ず同じハッシュを返すよう実装する必要があります。)

議論のハイライト

  • 配置先の変遷: 当初の配置先は標準ライブラリの container/hash.Set でした。その後 x/exp/container/sets.Hashed を経て、最終的に golang.org/x/exp/collection/hashed.Set になりました。まず x/exp で試す方針がとられています。
  • API の削減: 2026-09 の更新で、iterator を受け取る ContainsAll、InsertAll、DeleteAll が削除されました。
  • collection/set に統合しない理由: @willfaught は、set.Set に Hasher をオプションとして渡せるようにすればよいのではないかと提案しました。@adonovan は、#69230 の set.Set は組み込み map を中身が見える形でラップする型であり、独自の等価関係が必要なときは hashed.Set を使うという役割分担を説明しました。一部の型だけ通常の比較で済ませたい場合は maphash.ComparableHasher を使えばよい、とも述べています。
  • 命名と直列化: @seankhliao は Difference や SymmetricDifference といった名前が分かりにくいと指摘しました。@prattmic は JSON などへのマーシャラーを用意すべきではないかと問いかけました。どちらも、提案の変更には至っていません。
  • ドキュメントの Nit: @aclements は、「Union や Intersection が必要なら set パッケージのヘルパーを使う」という括弧書きについて、hashed.Set 自体にもそれらのメソッドがあるので紛らわしいと指摘しました。これは組み込み map に対する集合演算の話だと読み取れる表現にすべき、という趣旨です。

関連リンク