Skip to content

Commit

Permalink
Add a Clear() function to generic sets
Browse files Browse the repository at this point in the history
This is useful when a given set is shared through pointers, and
therefore can't be replaced with a new empty set. This replaces
set.Delete(set.UnsortedList()...), and allows the compiler to optimize
the function to a call to runtime.mapclear() when the set content type
is reflexive for ==. That optimization *is* currently accessible using

    s := Set[...]{}
    ...
    for i := range s {
        delete(s, i)
    }

but this circumvents the published API for sets; calling s.Delete(i)
instead can't be optimized in this fashion.

Alternatives considered but discarded include:

* turning sets into a pointer type (this isn't possible because
  pointer types can't be receivers)

* using a pointer receiver for the Clear() function, i.e.

    func (s *Set[T]) Clear() {
        *s = New[T]()
    }

  but this doesn't update shared references.

Signed-off-by: Stephen Kitt <skitt@redhat.com>

Kubernetes-commit: 57b0f7aa385b631c0be4702693a460cb93421702
  • Loading branch information
skitt authored and k8s-publishing-bot committed Mar 2, 2023
1 parent 66f7b89 commit 0993673
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 0 deletions.
14 changes: 14 additions & 0 deletions pkg/util/sets/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ func (s Set[T]) Delete(items ...T) Set[T] {
return s
}

// Clear empties the set.
// It is preferable to replace the set with a newly constructed set,
// but not all callers can do that (when there are other references to the map).
// In some cases the set *won't* be fully cleared, e.g. a Set[float32] containing NaN
// can't be cleared because NaN can't be removed.
// For sets containing items of a type that is reflexive for ==,
// this is optimized to a single call to runtime.mapclear().
func (s Set[T]) Clear() Set[T] {
for key := range s {
delete(s, key)
}
return s
}

// Has returns true if and only if item is contained in the set.
func (s Set[T]) Has(item T) bool {
_, contained := s[item]
Expand Down
51 changes: 51 additions & 0 deletions pkg/util/sets/set_generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,57 @@ func TestSetDeleteMultiples(t *testing.T) {

}

func TestSetClear(t *testing.T) {
s := sets.Set[string]{}
s.Insert("a", "b", "c")
if s.Len() != 3 {
t.Errorf("Expected len=3: %d", s.Len())
}

s.Clear()
if s.Len() != 0 {
t.Errorf("Expected len=0: %d", s.Len())
}
}

func TestSetClearWithSharedReference(t *testing.T) {
s := sets.Set[string]{}
s.Insert("a", "b", "c")
if s.Len() != 3 {
t.Errorf("Expected len=3: %d", s.Len())
}

m := s
s.Clear()
if s.Len() != 0 {
t.Errorf("Expected len=0 on the cleared set: %d", s.Len())
}
if m.Len() != 0 {
t.Errorf("Expected len=0 on the shared reference: %d", m.Len())
}
}

func TestSetClearInSeparateFunction(t *testing.T) {
s := sets.Set[string]{}
s.Insert("a", "b", "c")
if s.Len() != 3 {
t.Errorf("Expected len=3: %d", s.Len())
}

clearSetAndAdd(s, "d")
if s.Len() != 1 {
t.Errorf("Expected len=1: %d", s.Len())
}
if !s.Has("d") {
t.Errorf("Unexpected contents: %#v", s)
}
}

func clearSetAndAdd[T comparable](s sets.Set[T], a T) {
s.Clear()
s.Insert(a)
}

func TestNewSet(t *testing.T) {
s := sets.New("a", "b", "c")
if len(s) != 3 {
Expand Down

0 comments on commit 0993673

Please sign in to comment.