tailscale/util/set/smallset.go
Brad Fitzpatrick ef49e75b10 util/set: add SmallSet.SoleElement, fix bug, add more tests
This adds SmallSet.SoleElement, which I need in another repo for
efficiency. I added tests, but those tests failed because Add(1) +
Add(1) was promoting the first Add's sole element to a map of one
item. So fix that, and add more tests.

Updates tailscale/corp#29093

Change-Id: Iadd5ad08afe39721ee5449343095e389214d8389
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-05-29 13:06:29 -07:00

149 lines
3.4 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package set
import (
"iter"
"maps"
"tailscale.com/types/structs"
)
// SmallSet is a set that is optimized for reducing memory overhead when the
// expected size of the set is 0 or 1 elements.
//
// The zero value of SmallSet is a usable empty set.
//
// When storing a SmallSet in a map as a value type, it is important to re-assign
// the map entry after calling Add or Delete, as the SmallSet's representation
// may change.
//
// Copying a SmallSet by value may alias the previous value. Use the Clone method
// to create a new SmallSet with the same contents.
type SmallSet[T comparable] struct {
_ structs.Incomparable // to prevent == mistakes
one T // if non-zero, then single item in set
m Set[T] // if non-nil, the set of items, which might be size 1 if it's the zero value of T
}
// Values returns an iterator over the elements of the set.
// The iterator will yield the elements in no particular order.
func (s SmallSet[T]) Values() iter.Seq[T] {
if s.m != nil {
return maps.Keys(s.m)
}
var zero T
return func(yield func(T) bool) {
if s.one != zero {
yield(s.one)
}
}
}
// Contains reports whether e is in the set.
func (s SmallSet[T]) Contains(e T) bool {
if s.m != nil {
return s.m.Contains(e)
}
var zero T
return e != zero && s.one == e
}
// SoleElement returns the single value in the set, if the set has exactly one
// element.
//
// If the set is empty or has more than one element, ok will be false and e will
// be the zero value of T.
func (s SmallSet[T]) SoleElement() (e T, ok bool) {
return s.one, s.Len() == 1
}
// Add adds e to the set.
//
// When storing a SmallSet in a map as a value type, it is important to
// re-assign the map entry after calling Add or Delete, as the SmallSet's
// representation may change.
func (s *SmallSet[T]) Add(e T) {
var zero T
if s.m != nil {
s.m.Add(e)
return
}
// Non-zero elements can go into s.one.
if e != zero {
if s.one == zero {
s.one = e // Len 0 to Len 1
return
}
if s.one == e {
return // dup
}
}
// Need to make a multi map, either
// because we now have two items, or
// because e is the zero value.
s.m = Set[T]{}
if s.one != zero {
s.m.Add(s.one) // move single item to multi
}
s.m.Add(e) // add new item, possibly zero
s.one = zero
}
// Len reports the number of elements in the set.
func (s SmallSet[T]) Len() int {
var zero T
if s.m != nil {
return s.m.Len()
}
if s.one != zero {
return 1
}
return 0
}
// Delete removes e from the set.
//
// When storing a SmallSet in a map as a value type, it is important to
// re-assign the map entry after calling Add or Delete, as the SmallSet's
// representation may change.
func (s *SmallSet[T]) Delete(e T) {
var zero T
if s.m == nil {
if s.one == e {
s.one = zero
}
return
}
s.m.Delete(e)
// If the map size drops to zero, that means
// it only contained the zero value of T.
if s.m.Len() == 0 {
s.m = nil
return
}
// If the map size drops to one element and doesn't
// contain the zero value, we can switch back to the
// single-item representation.
if s.m.Len() == 1 {
for v := range s.m {
if v != zero {
s.one = v
s.m = nil
}
}
}
return
}
// Clone returns a copy of s that doesn't alias the original.
func (s SmallSet[T]) Clone() SmallSet[T] {
return SmallSet[T]{
one: s.one,
m: maps.Clone(s.m), // preserves nilness
}
}