From 45aeed33aa0e7feff4fe33577415c492c225f20a Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Thu, 11 Jan 2024 19:57:41 +0100 Subject: [PATCH 01/10] Feat: Added reactive SortedSet --- ds/reactive/sorted_set.go | 34 +++++ ds/reactive/sorted_set_impl.go | 251 +++++++++++++++++++++++++++++++++ ds/reactive/sorted_set_test.go | 96 +++++++++++++ 3 files changed, 381 insertions(+) create mode 100644 ds/reactive/sorted_set.go create mode 100644 ds/reactive/sorted_set_impl.go create mode 100644 ds/reactive/sorted_set_test.go diff --git a/ds/reactive/sorted_set.go b/ds/reactive/sorted_set.go new file mode 100644 index 00000000..12da96b8 --- /dev/null +++ b/ds/reactive/sorted_set.go @@ -0,0 +1,34 @@ +package reactive + +import ( + "cmp" +) + +// region SortedSet //////////////////////////////////////////////////////////////////////////////////////////////////// + +// SortedSet is a reactive Set implementation that allows consumers to subscribe to its changes and that keeps a sorted +// perception of its elements. +type SortedSet[ElementType comparable] interface { + // Set imports the methods of the Set interface. + Set[ElementType] + + // Ascending returns a slice of all elements of the set in ascending order. + Ascending() []ElementType + + // Descending returns a slice of all elements of the set in descending order. + Descending() []ElementType + + // HeaviestElement returns the element with the heaviest weight. + HeaviestElement() ReadableVariable[ElementType] + + // LightestElement returns the element with the lightest weight. + LightestElement() ReadableVariable[ElementType] +} + +// NewSortedSet creates a new SortedSet instance that sorts its elements by the given weightVariable. It is possible to +// optionally provide a tieBreaker function that is used to break ties between elements with the same weight. +func NewSortedSet[ElementType comparable, WeightType cmp.Ordered](weightVariable func(element ElementType) Variable[WeightType], optTieBreaker ...func(left, right ElementType) int) SortedSet[ElementType] { + return newSortedSet(weightVariable, optTieBreaker...) +} + +// endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/ds/reactive/sorted_set_impl.go b/ds/reactive/sorted_set_impl.go new file mode 100644 index 00000000..6cf8c6f7 --- /dev/null +++ b/ds/reactive/sorted_set_impl.go @@ -0,0 +1,251 @@ +package reactive + +import ( + "cmp" + + "github.com/iotaledger/hive.go/ds" + "github.com/iotaledger/hive.go/ds/shrinkingmap" + "github.com/iotaledger/hive.go/lo" + "github.com/iotaledger/hive.go/runtime/syncutils" +) + +// region sortedSet //////////////////////////////////////////////////////////////////////////////////////////////////// + +// sortedSet is the default implementation of the SortedSet interface. +type sortedSet[ElementType comparable, WeightType cmp.Ordered] struct { + // Set imports the methods of the Set interface. + Set[ElementType] + + // elements is a map of all elements that are part of the set. + elements *shrinkingmap.ShrinkingMap[ElementType, *sortedSetElement[ElementType, WeightType]] + + // sortedElements is a slice of all elements that are part of the set, sorted by their weight. + sortedElements []*sortedSetElement[ElementType, WeightType] + + // heaviestElement is a reference to the element with the heaviest weight. + heaviestElement Variable[ElementType] + + // lightestElement is a reference to the element with the lightest weight. + lightestElement Variable[ElementType] + + // weightVariable is the function that is used to retrieve the weight of an element. + weightVariable func(element ElementType) Variable[WeightType] + + // tieBreaker is the function that is used to break ties between elements with the same weight. + tieBreaker func(left, right ElementType) int + + // mutex is used to synchronize access to the sortedElements slice. + mutex syncutils.RWMutex +} + +// NewSortedSet creates a new SortedSet instance that sorts its elements by the given weightVariable. It is possible to +// optionally provide a tieBreaker function that is used to break ties between elements with the same weight. +func newSortedSet[ElementType comparable, WeightType cmp.Ordered](weightVariable func(element ElementType) Variable[WeightType], optTieBreaker ...func(left, right ElementType) int) *sortedSet[ElementType, WeightType] { + s := &sortedSet[ElementType, WeightType]{ + Set: NewSet[ElementType](), + elements: shrinkingmap.New[ElementType, *sortedSetElement[ElementType, WeightType]](), + sortedElements: make([]*sortedSetElement[ElementType, WeightType], 0), + heaviestElement: NewVariable[ElementType](), + lightestElement: NewVariable[ElementType](), + weightVariable: weightVariable, + tieBreaker: lo.First(optTieBreaker, defaultTieBreaker[ElementType]), + } + + s.OnUpdate(func(appliedMutations ds.SetMutations[ElementType]) { + appliedMutations.AddedElements().Range(s.addSorted) + appliedMutations.DeletedElements().Range(s.deleteSorted) + }) + + return s +} + +// Ascending returns a slice of all elements of the set in ascending order. +func (s *sortedSet[ElementType, WeightType]) Ascending() (sortedSlice []ElementType) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + if sortedElementsCount := len(s.sortedElements); sortedElementsCount > 0 { + sortedSlice = make([]ElementType, sortedElementsCount) + + for i, sortedElement := range s.sortedElements { + sortedSlice[sortedElementsCount-i-1] = sortedElement.element + } + } + + return sortedSlice +} + +// Descending returns a slice of all elements of the set in descending order. +func (s *sortedSet[ElementType, WeightType]) Descending() (sortedSlice []ElementType) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + if sortedElementsCount := len(s.sortedElements); sortedElementsCount > 0 { + sortedSlice = make([]ElementType, sortedElementsCount) + + for i, sortedElement := range s.sortedElements { + sortedSlice[i] = sortedElement.element + } + } + + return sortedSlice +} + +// HeaviestElement returns the element with the heaviest weight. +func (s *sortedSet[ElementType, WeightType]) HeaviestElement() ReadableVariable[ElementType] { + return s.heaviestElement +} + +// LightestElement returns the element with the lightest weight. +func (s *sortedSet[ElementType, WeightType]) LightestElement() ReadableVariable[ElementType] { + return s.lightestElement +} + +// addSorted adds the given element to the sortedElements slice. +func (s *sortedSet[ElementType, WeightType]) addSorted(element ElementType) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if listElement, created := s.elements.GetOrCreate(element, func() *sortedSetElement[ElementType, WeightType] { + return newSortedSetElement(element, s) + }); created { + listElement.unsubscribeFromWeightUpdates = s.weightVariable(element).OnUpdate(func(_ WeightType, newWeight WeightType) { + // only lock if this is not the initial update + if listElement.unsubscribeFromWeightUpdates != nil { + s.mutex.Lock() + defer s.mutex.Unlock() + } + + listElement.weight = newWeight + + s.updatePosition(listElement) + }, true) + } +} + +// deleteSorted deletes the given element from the sortedElements slice. +func (s *sortedSet[ElementType, WeightType]) deleteSorted(element ElementType) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if deletedElement, deleted := s.elements.DeleteAndReturn(element); deleted { + // unsubscribe from weight updates + deletedElement.unsubscribeFromWeightUpdates() + + // shift all elements to the right of the deleted element one position to the left + for i := deletedElement.index; i < len(s.sortedElements)-1; i++ { + s.sortedElements[i] = s.sortedElements[i+1] + s.sortedElements[i].index-- + } + + // prevent memory leak and shrink slice + s.sortedElements[len(s.sortedElements)-1] = nil + s.sortedElements = s.sortedElements[:len(s.sortedElements)-1] + + // update heaviest and lightest element + if deletedElement.index == 0 { + if len(s.sortedElements) > 0 { + s.heaviestElement.Set(s.sortedElements[0].element) + } else { + s.heaviestElement.Set(*new(ElementType)) + } + } + if deletedElement.index == len(s.sortedElements) { + if len(s.sortedElements) > 0 { + s.lightestElement.Set(s.sortedElements[len(s.sortedElements)-1].element) + } else { + s.lightestElement.Set(*new(ElementType)) + } + } + } +} + +// updatePosition updates the position of the given element in the sortedElements slice. +func (s *sortedSet[ElementType, WeightType]) updatePosition(element *sortedSetElement[ElementType, WeightType]) (moved bool) { + // update heaviest and lightest references after we are done moving the element + defer func(oldIndex int) { + if moved { + if oldIndex == 0 { + s.heaviestElement.Set(s.sortedElements[0].element) + } + + if oldIndex == len(s.sortedElements)-1 { + s.lightestElement.Set(s.sortedElements[len(s.sortedElements)-1].element) + } + } + + if element.index == 0 { + s.heaviestElement.Set(element.element) + } + + if element.index == len(s.sortedElements)-1 { + s.lightestElement.Set(element.element) + } + }(element.index) + + // try to move the element to the left first + for ; element.index != 0; moved = true { + if !s.swap(s.sortedElements[element.index-1], element) { + break + } + } + + // if the element was not moved to the left, try to move it to the right + if !moved { + for ; element.index != len(s.sortedElements)-1; moved = true { + if !s.swap(element, s.sortedElements[element.index+1]) { + break + } + } + } + + return +} + +// swap swaps the position of the two given elements in the sortedElements slice. +func (s *sortedSet[ElementType, WeightType]) swap(left *sortedSetElement[ElementType, WeightType], right *sortedSetElement[ElementType, WeightType]) (swapped bool) { + if swapped = (left.weight < right.weight) || (left.weight == right.weight && s.tieBreaker(left.element, right.element) < 0); swapped { + s.sortedElements[left.index], s.sortedElements[right.index] = s.sortedElements[right.index], s.sortedElements[left.index] + left.index, right.index = right.index, left.index + } + + return swapped +} + +// defaultTieBreaker is the default tie-breaker function that is used to break ties between elements with the same weight. +func defaultTieBreaker[ElementType comparable](left ElementType, right ElementType) int { + return 0 +} + +// endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// region sortedSetElement ///////////////////////////////////////////////////////////////////////////////////////////// + +// sortedSetElement is an element of the sortedElements slice. +type sortedSetElement[ElementType comparable, WeightType cmp.Ordered] struct { + // element is the element that is part of the set. + element ElementType + + // weight is the weight of the element. + weight WeightType + + // index is the index of the element in the sortedElements slice. + index int + + // unsubscribeFromWeightUpdates is the function that is used to unsubscribe from weight updates. + unsubscribeFromWeightUpdates func() +} + +// newSortedSetElement creates a new sortedSetElement instance. +func newSortedSetElement[WeightType cmp.Ordered, ElementType comparable](element ElementType, sortedSet *sortedSet[ElementType, WeightType]) *sortedSetElement[ElementType, WeightType] { + s := &sortedSetElement[ElementType, WeightType]{ + element: element, + index: len(sortedSet.sortedElements), + } + + sortedSet.sortedElements = append(sortedSet.sortedElements, s) + + return s +} + +// endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/ds/reactive/sorted_set_test.go b/ds/reactive/sorted_set_test.go new file mode 100644 index 00000000..e30ace50 --- /dev/null +++ b/ds/reactive/sorted_set_test.go @@ -0,0 +1,96 @@ +package reactive + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_SortedSet(t *testing.T) { + element1 := newSortableElement("1st", 1) + element2 := newSortableElement("2nd", 2) + element3 := newSortableElement("3rd", 3) + + testSet := NewSortedSet((*sortableElement).weight) + requireOrder(t, []*sortableElement{}, testSet) + + testSet.Add(element1) + requireOrder(t, []*sortableElement{element1}, testSet) + + testSet.Add(element2) + requireOrder(t, []*sortableElement{element2, element1}, testSet) + + testSet.Add(element3) + requireOrder(t, []*sortableElement{element3, element2, element1}, testSet) + + element2.Weight.Set(5) + requireOrder(t, []*sortableElement{element2, element3, element1}, testSet) + + element1.Weight.Set(4) + requireOrder(t, []*sortableElement{element2, element1, element3}, testSet) + + element2.Weight.Set(3) + requireOrder(t, []*sortableElement{element1, element2, element3}, testSet) + + testSet.Delete(element2) + requireOrder(t, []*sortableElement{element1, element3}, testSet) + + testSet.Delete(element1) + requireOrder(t, []*sortableElement{element3}, testSet) + + testSet.Add(element1) + requireOrder(t, []*sortableElement{element1, element3}, testSet) + + testSet.Delete(element3) + requireOrder(t, []*sortableElement{element1}, testSet) + + testSet.Delete(element1) + requireOrder(t, []*sortableElement{}, testSet) +} + +func requireOrder[ElementType comparable](t *testing.T, expectedElements []ElementType, sortedSet SortedSet[ElementType]) { + descendingElements := sortedSet.Descending() + require.Equal(t, len(expectedElements), len(descendingElements)) + + for i, expectedElement := range expectedElements { + require.Equal(t, expectedElement, descendingElements[i]) + } + + ascendingElements := sortedSet.Ascending() + require.Equal(t, len(expectedElements), len(ascendingElements)) + + if len(expectedElements) > 0 { + require.Equal(t, expectedElements[0], sortedSet.HeaviestElement().Get()) + require.Equal(t, expectedElements[len(expectedElements)-1], sortedSet.LightestElement().Get()) + } else { + require.Equal(t, *new(ElementType), sortedSet.HeaviestElement().Get()) + require.Equal(t, *new(ElementType), sortedSet.LightestElement().Get()) + } +} + +type sortableElement struct { + value string + Weight Variable[int] +} + +func newSortableElement(value string, weight int) *sortableElement { + return &sortableElement{ + value: value, + Weight: NewVariable[int]().Init(weight), + } +} + +func (t *sortableElement) weight() Variable[int] { + return t.Weight +} + +func (t *sortableElement) compare(other *sortableElement) int { + switch { + case t.value < other.value: + return -1 + case t.value > other.value: + return 1 + default: + return 0 + } +} From 3ed5049f53c28441afdb0ad843aac377d817e29d Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Thu, 11 Jan 2024 20:01:20 +0100 Subject: [PATCH 02/10] Feat: go mod tidy --- ds/go.mod | 4 ++++ ds/go.sum | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/ds/go.mod b/ds/go.mod index c8e7dbe2..e2af7b43 100644 --- a/ds/go.mod +++ b/ds/go.mod @@ -17,7 +17,11 @@ require ( github.com/ethereum/go-ethereum v1.13.8 // indirect github.com/holiman/uint256 v1.2.4 // indirect github.com/iancoleman/orderedmap v0.3.0 // indirect + github.com/iotaledger/hive.go/stringify v0.0.0-20231223024336-954578715888 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sasha-s/go-deadlock v0.3.1 // indirect golang.org/x/sys v0.15.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/ds/go.sum b/ds/go.sum index 6ea43088..ce7e6442 100644 --- a/ds/go.sum +++ b/ds/go.sum @@ -1,5 +1,6 @@ github.com/btcsuite/btcd/btcec/v2 v2.2.1 h1:xP60mv8fvp+0khmrN0zTdPC3cNm24rfeE6lh2R/Yv3E= github.com/btcsuite/btcd/btcec/v2 v2.2.1/go.mod h1:9/CSmJxmuvqzX9Wh2fXMWToLOHhPd11lSPuIupwTkI8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= @@ -20,16 +21,23 @@ github.com/iotaledger/hive.go/runtime v0.0.0-20231223024501-693ba063d354 h1:Q3I8 github.com/iotaledger/hive.go/runtime v0.0.0-20231223024501-693ba063d354/go.mod h1:AzqO/YvIdFTZoK8r9UWHvxrqaWeWxDN+rFf+V7b/6gY= github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231223024501-693ba063d354 h1:THvgcgrA9Dagi/pUY8qZvFJ4RzpICCMJR2PDvL5utQ0= github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231223024501-693ba063d354/go.mod h1:LkvNBG7PeoQgCCSB/eMNX8kSHTXtRN10kFciLdMvcgc= +github.com/iotaledger/hive.go/stringify v0.0.0-20231223024336-954578715888 h1:qsPDAoCrvmuKKwWQZ/wcXGvDXGMVE6E4scQeP68U7to= +github.com/iotaledger/hive.go/stringify v0.0.0-20231223024336-954578715888/go.mod h1:FTo/UWzNYgnQ082GI9QVM9HFDERqf9rw9RivNpqrnTs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 h1:jik8PHtAIsPlCRJjJzl4udgEf7hawInF9texMeO2jrU= +github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= From d68637726a80c7037ebd961d24c1e46206f0bb42 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Thu, 11 Jan 2024 20:12:17 +0100 Subject: [PATCH 03/10] Refactor: addressed linter error --- ds/reactive/sorted_set_impl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ds/reactive/sorted_set_impl.go b/ds/reactive/sorted_set_impl.go index 6cf8c6f7..76a884bd 100644 --- a/ds/reactive/sorted_set_impl.go +++ b/ds/reactive/sorted_set_impl.go @@ -213,7 +213,7 @@ func (s *sortedSet[ElementType, WeightType]) swap(left *sortedSetElement[Element } // defaultTieBreaker is the default tie-breaker function that is used to break ties between elements with the same weight. -func defaultTieBreaker[ElementType comparable](left ElementType, right ElementType) int { +func defaultTieBreaker[ElementType comparable](_ ElementType, _ ElementType) int { return 0 } From 822f4326ef0e59a0025f65fcda880fda15d16414 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:56:00 +0100 Subject: [PATCH 04/10] Refactor: added interface instead of tie breaker --- ds/reactive/sorted_set.go | 11 ++++++----- ds/reactive/sorted_set_impl.go | 25 +++++++++++++------------ ds/reactive/sorted_set_test.go | 6 +++++- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/ds/reactive/sorted_set.go b/ds/reactive/sorted_set.go index 12da96b8..ca9d447e 100644 --- a/ds/reactive/sorted_set.go +++ b/ds/reactive/sorted_set.go @@ -7,7 +7,8 @@ import ( // region SortedSet //////////////////////////////////////////////////////////////////////////////////////////////////// // SortedSet is a reactive Set implementation that allows consumers to subscribe to its changes and that keeps a sorted -// perception of its elements. +// perception of its elements. If the ElementType implements a Less method, it will be used to break ties between +// elements with the same weight. type SortedSet[ElementType comparable] interface { // Set imports the methods of the Set interface. Set[ElementType] @@ -25,10 +26,10 @@ type SortedSet[ElementType comparable] interface { LightestElement() ReadableVariable[ElementType] } -// NewSortedSet creates a new SortedSet instance that sorts its elements by the given weightVariable. It is possible to -// optionally provide a tieBreaker function that is used to break ties between elements with the same weight. -func NewSortedSet[ElementType comparable, WeightType cmp.Ordered](weightVariable func(element ElementType) Variable[WeightType], optTieBreaker ...func(left, right ElementType) int) SortedSet[ElementType] { - return newSortedSet(weightVariable, optTieBreaker...) +// NewSortedSet creates a new SortedSet instance that sorts its elements by the given weightVariable. If the ElementType +// implements a Less method, it will be used to break ties between elements with the same weight. +func NewSortedSet[ElementType comparable, WeightType cmp.Ordered](weightVariable func(element ElementType) Variable[WeightType]) SortedSet[ElementType] { + return newSortedSet(weightVariable) } // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/ds/reactive/sorted_set_impl.go b/ds/reactive/sorted_set_impl.go index 76a884bd..2cbfce76 100644 --- a/ds/reactive/sorted_set_impl.go +++ b/ds/reactive/sorted_set_impl.go @@ -5,7 +5,6 @@ import ( "github.com/iotaledger/hive.go/ds" "github.com/iotaledger/hive.go/ds/shrinkingmap" - "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/runtime/syncutils" ) @@ -31,16 +30,13 @@ type sortedSet[ElementType comparable, WeightType cmp.Ordered] struct { // weightVariable is the function that is used to retrieve the weight of an element. weightVariable func(element ElementType) Variable[WeightType] - // tieBreaker is the function that is used to break ties between elements with the same weight. - tieBreaker func(left, right ElementType) int - // mutex is used to synchronize access to the sortedElements slice. mutex syncutils.RWMutex } -// NewSortedSet creates a new SortedSet instance that sorts its elements by the given weightVariable. It is possible to -// optionally provide a tieBreaker function that is used to break ties between elements with the same weight. -func newSortedSet[ElementType comparable, WeightType cmp.Ordered](weightVariable func(element ElementType) Variable[WeightType], optTieBreaker ...func(left, right ElementType) int) *sortedSet[ElementType, WeightType] { +// NewSortedSet creates a new SortedSet instance that sorts its elements by the given weightVariable. If the ElementType +// implements a Less method, it will be used to break ties between elements with the same weight. +func newSortedSet[ElementType comparable, WeightType cmp.Ordered](weightVariable func(element ElementType) Variable[WeightType]) *sortedSet[ElementType, WeightType] { s := &sortedSet[ElementType, WeightType]{ Set: NewSet[ElementType](), elements: shrinkingmap.New[ElementType, *sortedSetElement[ElementType, WeightType]](), @@ -48,7 +44,6 @@ func newSortedSet[ElementType comparable, WeightType cmp.Ordered](weightVariable heaviestElement: NewVariable[ElementType](), lightestElement: NewVariable[ElementType](), weightVariable: weightVariable, - tieBreaker: lo.First(optTieBreaker, defaultTieBreaker[ElementType]), } s.OnUpdate(func(appliedMutations ds.SetMutations[ElementType]) { @@ -204,7 +199,13 @@ func (s *sortedSet[ElementType, WeightType]) updatePosition(element *sortedSetEl // swap swaps the position of the two given elements in the sortedElements slice. func (s *sortedSet[ElementType, WeightType]) swap(left *sortedSetElement[ElementType, WeightType], right *sortedSetElement[ElementType, WeightType]) (swapped bool) { - if swapped = (left.weight < right.weight) || (left.weight == right.weight && s.tieBreaker(left.element, right.element) < 0); swapped { + if swapped = left.weight < right.weight; !swapped && left.weight == right.weight { + if leftAsLessable, ok := ((any)(left.element)).(lessable[ElementType]); ok { + swapped = leftAsLessable.Less(right.element) + } + } + + if swapped { s.sortedElements[left.index], s.sortedElements[right.index] = s.sortedElements[right.index], s.sortedElements[left.index] left.index, right.index = right.index, left.index } @@ -212,9 +213,9 @@ func (s *sortedSet[ElementType, WeightType]) swap(left *sortedSetElement[Element return swapped } -// defaultTieBreaker is the default tie-breaker function that is used to break ties between elements with the same weight. -func defaultTieBreaker[ElementType comparable](_ ElementType, _ ElementType) int { - return 0 +// lessable is an interface that allows consumers to define a custom less function for a type. +type lessable[T any] interface { + Less(other T) bool } // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/ds/reactive/sorted_set_test.go b/ds/reactive/sorted_set_test.go index e30ace50..3ec66800 100644 --- a/ds/reactive/sorted_set_test.go +++ b/ds/reactive/sorted_set_test.go @@ -30,7 +30,7 @@ func Test_SortedSet(t *testing.T) { requireOrder(t, []*sortableElement{element2, element1, element3}, testSet) element2.Weight.Set(3) - requireOrder(t, []*sortableElement{element1, element2, element3}, testSet) + requireOrder(t, []*sortableElement{element1, element3, element2}, testSet) testSet.Delete(element2) requireOrder(t, []*sortableElement{element1, element3}, testSet) @@ -80,6 +80,10 @@ func newSortableElement(value string, weight int) *sortableElement { } } +func (t *sortableElement) Less(other *sortableElement) bool { + return t.value < other.value +} + func (t *sortableElement) weight() Variable[int] { return t.Weight } From 623cd3fb800b998493f30f10736232e88251509e Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 17 Jan 2024 10:57:36 +0100 Subject: [PATCH 05/10] Feat: add support for nil callbacks in Batch --- lo/batch.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lo/batch.go b/lo/batch.go index 3b411aba..f40e5721 100644 --- a/lo/batch.go +++ b/lo/batch.go @@ -3,7 +3,9 @@ package lo func Batch(callbacks ...func()) func() { return func() { for _, callback := range callbacks { - callback() + if callback != nil { + callback() + } } } } From 6909080c05bc981d4f546eda0e6f3a1062f1806d Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 24 Jan 2024 20:48:20 +0100 Subject: [PATCH 06/10] Feat: added test for ascending --- ds/reactive/sorted_set_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ds/reactive/sorted_set_test.go b/ds/reactive/sorted_set_test.go index 3ec66800..01ea7c88 100644 --- a/ds/reactive/sorted_set_test.go +++ b/ds/reactive/sorted_set_test.go @@ -51,13 +51,15 @@ func Test_SortedSet(t *testing.T) { func requireOrder[ElementType comparable](t *testing.T, expectedElements []ElementType, sortedSet SortedSet[ElementType]) { descendingElements := sortedSet.Descending() require.Equal(t, len(expectedElements), len(descendingElements)) - for i, expectedElement := range expectedElements { require.Equal(t, expectedElement, descendingElements[i]) } ascendingElements := sortedSet.Ascending() require.Equal(t, len(expectedElements), len(ascendingElements)) + for i, expectedElement := range expectedElements { + require.Equal(t, expectedElement, ascendingElements[len(expectedElements)-i-1]) + } if len(expectedElements) > 0 { require.Equal(t, expectedElements[0], sortedSet.HeaviestElement().Get()) From 00136d8df581e0e4b70ba3047b7e0cd700a9b5f9 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:23:11 +0100 Subject: [PATCH 07/10] Feat: refactored code --- ds/reactive/sorted_set_impl.go | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/ds/reactive/sorted_set_impl.go b/ds/reactive/sorted_set_impl.go index 2cbfce76..84fa54bc 100644 --- a/ds/reactive/sorted_set_impl.go +++ b/ds/reactive/sorted_set_impl.go @@ -158,22 +158,20 @@ func (s *sortedSet[ElementType, WeightType]) deleteSorted(element ElementType) { // updatePosition updates the position of the given element in the sortedElements slice. func (s *sortedSet[ElementType, WeightType]) updatePosition(element *sortedSetElement[ElementType, WeightType]) (moved bool) { // update heaviest and lightest references after we are done moving the element - defer func(oldIndex int) { - if moved { - if oldIndex == 0 { - s.heaviestElement.Set(s.sortedElements[0].element) - } - - if oldIndex == len(s.sortedElements)-1 { - s.lightestElement.Set(s.sortedElements[len(s.sortedElements)-1].element) - } - } - - if element.index == 0 { + defer func(fromIndex int) { + if moved && fromIndex == 0 { + // moved away from the heaviest element + s.heaviestElement.Set(s.sortedElements[0].element) + } else if element.index == 0 { + // moved towards the heaviest element s.heaviestElement.Set(element.element) } - if element.index == len(s.sortedElements)-1 { + if moved && fromIndex == len(s.sortedElements)-1 { + // moved away from the lightest element + s.lightestElement.Set(s.sortedElements[len(s.sortedElements)-1].element) + } else if element.index == len(s.sortedElements)-1 { + // moved towards the lightest element s.lightestElement.Set(element.element) } }(element.index) From bd3f8750672e80f2fa02b9d39a49a605da95700c Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:51:08 +0100 Subject: [PATCH 08/10] Feat: upgrade go.mod --- ds/go.mod | 4 ++++ ds/go.sum | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/ds/go.mod b/ds/go.mod index ad9d5a50..f3c99b23 100644 --- a/ds/go.mod +++ b/ds/go.mod @@ -17,7 +17,11 @@ require ( github.com/ethereum/go-ethereum v1.13.11 // indirect github.com/holiman/uint256 v1.2.4 // indirect github.com/iancoleman/orderedmap v0.3.0 // indirect + github.com/iotaledger/hive.go/stringify v0.0.0-20240124155714-a0713583cf95 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sasha-s/go-deadlock v0.3.1 // indirect golang.org/x/sys v0.16.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/ds/go.sum b/ds/go.sum index c11c6b91..81b026cc 100644 --- a/ds/go.sum +++ b/ds/go.sum @@ -1,5 +1,6 @@ github.com/btcsuite/btcd/btcec/v2 v2.2.1 h1:xP60mv8fvp+0khmrN0zTdPC3cNm24rfeE6lh2R/Yv3E= github.com/btcsuite/btcd/btcec/v2 v2.2.1/go.mod h1:9/CSmJxmuvqzX9Wh2fXMWToLOHhPd11lSPuIupwTkI8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= @@ -20,16 +21,23 @@ github.com/iotaledger/hive.go/runtime v0.0.0-20240124155826-defd9fcfcd4a h1:qD7p github.com/iotaledger/hive.go/runtime v0.0.0-20240124155826-defd9fcfcd4a/go.mod h1:+AgPOfvlzPCsA78rvMp2zdw0tdiaaOCEcFQCjaY2gz4= github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20240124155826-defd9fcfcd4a h1:/DPh6TgzNB99Z3NeXXU7M5wdlbKwIRFhlsrdZYMBTjE= github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20240124155826-defd9fcfcd4a/go.mod h1:bpJrJ0zYJqHpk8zIB5Sz7r9KCwpgKBaqHby5Uoouipg= +github.com/iotaledger/hive.go/stringify v0.0.0-20240124155714-a0713583cf95 h1:1yjQz9XpCEUL12KGOC1O2kHikU+huAXZSveN0cNglaM= +github.com/iotaledger/hive.go/stringify v0.0.0-20240124155714-a0713583cf95/go.mod h1:FTo/UWzNYgnQ082GI9QVM9HFDERqf9rw9RivNpqrnTs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 h1:jik8PHtAIsPlCRJjJzl4udgEf7hawInF9texMeO2jrU= +github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= From 3c7d645e420f3bb0f0982edb58f29a612ab31286 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 24 Jan 2024 22:13:53 +0100 Subject: [PATCH 09/10] Fix: fixed linter settings --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index dcfd569d..f73f15c2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -26,6 +26,7 @@ linters-settings: # exclude the ierrors.go file itself files: - "!**/ierrors.go" + - "!**/ierrors_no_stacktrace.go" deny: - pkg: "errors" desc: Should be replaced with "github.com/iotaledger/hive.go/ierrors" package From 873aab7e5115b4c4d59535c11cbc6a2a5082836c Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:55:23 +0800 Subject: [PATCH 10/10] Add comments for captured variable --- ds/reactive/sorted_set_impl.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ds/reactive/sorted_set_impl.go b/ds/reactive/sorted_set_impl.go index 84fa54bc..12b48d69 100644 --- a/ds/reactive/sorted_set_impl.go +++ b/ds/reactive/sorted_set_impl.go @@ -162,7 +162,7 @@ func (s *sortedSet[ElementType, WeightType]) updatePosition(element *sortedSetEl if moved && fromIndex == 0 { // moved away from the heaviest element s.heaviestElement.Set(s.sortedElements[0].element) - } else if element.index == 0 { + } else if element.index == 0 { // We check the index of the element after it is moved. // moved towards the heaviest element s.heaviestElement.Set(element.element) } @@ -170,11 +170,11 @@ func (s *sortedSet[ElementType, WeightType]) updatePosition(element *sortedSetEl if moved && fromIndex == len(s.sortedElements)-1 { // moved away from the lightest element s.lightestElement.Set(s.sortedElements[len(s.sortedElements)-1].element) - } else if element.index == len(s.sortedElements)-1 { + } else if element.index == len(s.sortedElements)-1 { // We check the index of the element after it is moved. // moved towards the lightest element s.lightestElement.Set(element.element) } - }(element.index) + }(element.index) // We capture the index of the element before it is moved. // try to move the element to the left first for ; element.index != 0; moved = true {