Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Added reactive SortedSet #630

Merged
merged 11 commits into from
Jan 25, 2024
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions ds/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
8 changes: 8 additions & 0 deletions ds/go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand All @@ -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=
Expand Down
35 changes: 35 additions & 0 deletions ds/reactive/sorted_set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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. 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]

// 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. 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 ///////////////////////////////////////////////////////////////////////////////////////////////////////////
250 changes: 250 additions & 0 deletions ds/reactive/sorted_set_impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package reactive

import (
"cmp"

"github.com/iotaledger/hive.go/ds"
"github.com/iotaledger/hive.go/ds/shrinkingmap"
"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]

// 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. 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]](),
sortedElements: make([]*sortedSetElement[ElementType, WeightType], 0),
heaviestElement: NewVariable[ElementType](),
lightestElement: NewVariable[ElementType](),
weightVariable: weightVariable,
}

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(fromIndex int) {
if moved && fromIndex == 0 {
// moved away from the heaviest element
s.heaviestElement.Set(s.sortedElements[0].element)
} 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)
}

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 { // We check the index of the element after it is moved.
// moved towards the lightest element
s.lightestElement.Set(element.element)
}
}(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 {
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; !swapped && left.weight == right.weight {
piotrm50 marked this conversation as resolved.
Show resolved Hide resolved
if leftAsLessable, ok := ((any)(left.element)).(lessable[ElementType]); ok {
jonastheis marked this conversation as resolved.
Show resolved Hide resolved
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
}

return swapped
}

// 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 ///////////////////////////////////////////////////////////////////////////////////////////////////////////

// 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 ///////////////////////////////////////////////////////////////////////////////////////////////////////////
Loading
Loading