Skip to content

Commit

Permalink
Support Go iterators (#46)
Browse files Browse the repository at this point in the history
Iterators were introduced in Go 1.23. They allow `for` loops over custom sequences.

There are some breaking changes to the API, which is why it's being releases as v3. Notable changes are:

- `All()` no longer exists. You can use the now native: `maps.Collect(m.AllFromFront())` for elements, or `slices.Collect(m.Keys())` for keys.
- `Iterator()` and `ReverseIterator()` are now `AllFromFront()` and `AllFromBack()` respectively.
- `Keys()` now returns an `iter.Seq` iterator instead of a list of keys. This should be much more performant on large maps. It is also more consistent with `maps.Keys` from the standard library.
- Added `Values()` which returns an `iter.Seq` iterator. It is also consistent with `maps.Values` from the standard library.

Fixes #44
  • Loading branch information
pd93 authored Dec 4, 2024
1 parent 378040e commit cc923df
Show file tree
Hide file tree
Showing 7 changed files with 1,379 additions and 30 deletions.
11 changes: 10 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
pull_request:

jobs:
unit-tests:
unit-tests-v2:
strategy:
matrix:
go:
Expand All @@ -19,3 +19,12 @@ jobs:
go-version: ${{ matrix.go }}
- run: go test -v ./...
- run: cd v2 && go test -v ./...

unit-tests-v3:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '^1.23'
- run: cd v3 && go test -v ./...
67 changes: 38 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# 🔃 github.com/elliotchance/orderedmap/v2 [![GoDoc](https://godoc.org/github.com/elliotchance/orderedmap/v2?status.svg)](https://godoc.org/github.com/elliotchance/orderedmap/v2)
# 🔃 github.com/elliotchance/orderedmap/v3 [![GoDoc](https://godoc.org/github.com/elliotchance/orderedmap/v3?status.svg)](https://godoc.org/github.com/elliotchance/orderedmap/v3)

## Basic Usage

An `*OrderedMap` is a high performance ordered map that maintains amortized O(1)
for `Set`, `Get`, `Delete` and `Len`:

```go
import "github.com/elliotchance/orderedmap/v2"
import "github.com/elliotchance/orderedmap/v3"

func main() {
m := orderedmap.NewOrderedMap[string, any]()
Expand All @@ -19,26 +19,55 @@ func main() {
}
```

*Note: v2 requires Go v1.18 for generics.* If you need to support Go 1.17 or
below, you can use v1.
> [!NOTE]
>
> - _v3 requires Go v1.23_ - If you need to support Go 1.18-1.22, you can use v2.
> - _v2 requires Go v1.18 for generics_ - If you need to support Go 1.17 or below, you can use v1.
Internally an `*OrderedMap` uses the composite type
[map](https://go.dev/blog/maps) combined with a
trimmed down linked list to maintain the order.

## Iterating

Be careful using `Keys()` as it will create a copy of all of the keys so it's
only suitable for a small number of items:
The following methods all return
[iterators](https://go.dev/doc/go1.23#iterators) that can be used to loop over
elements in an ordered map:

- `AllFromFront()`
- `AllFromBack()`
- `Keys()`
- `Values()`

```go
for _, key := range m.Keys() {
value, _:= m.Get(key)
// Iterate through all elements from oldest to newest:
for key, value := range m.AllFromFront() {
fmt.Println(key, value)
}
```

For larger maps you should use `Front()` or `Back()` to iterate per element:
Iterators are safe to use bidirectionally, and will return `nil` once it goes
beyond the first or last item. If the map is changing while the iteration is
in-flight it may produce unexpected behavior.

If you want to get a slice of the map keys or values, you can use the standard
`slices.Collect` method with the iterator returned from `Keys()` or `Values()`:

```go
fmt.Println(slices.Collect(m.Keys())
// [A B C]
```

Likewise, calling `maps.Collect` on the iterator returned from `AllFromFront()`
will create a regular unordered map from the ordered one:

```go
fmt.Println(maps.Collect(m.AllFromFront())
// [A:1 B:2 C:3]
```

If you don't want to use iterators, you can also manually loop over the elements
using `Front()` or `Back()` with `Next()`:

```go
// Iterate through all elements from oldest to newest:
Expand All @@ -51,23 +80,3 @@ for el := m.Back(); el != nil; el = el.Prev() {
fmt.Println(el.Key, el.Value)
}
```

In case you're using Go 1.23, you can also [iterate with
`range`](https://go.dev/doc/go1.23#iterators) by using `Iterator()` or
`ReverseIterator()` methods:

```go
for key, value := range m.Iterator() {
fmt.Println(key, value)
}

for key, value := range m.ReverseIterator() {
fmt.Println(key, value)
}
```

The iterator is safe to use bidirectionally, and will return `nil` once it goes
beyond the first or last item.

If the map is changing while the iteration is in-flight it may produce
unexpected behavior.
11 changes: 11 additions & 0 deletions v3/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/elliotchance/orderedmap/v3

go 1.23.0

require github.com/stretchr/testify v1.7.1

require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
11 changes: 11 additions & 0 deletions v3/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
95 changes: 95 additions & 0 deletions v3/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package orderedmap

// Element is an element of a null terminated (non circular) intrusive doubly linked list that contains the key of the correspondent element in the ordered map too.
type Element[K comparable, V any] struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element[K, V]

// The key that corresponds to this element in the ordered map.
Key K

// The value stored with this element.
Value V
}

// Next returns the next list element or nil.
func (e *Element[K, V]) Next() *Element[K, V] {
return e.next
}

// Prev returns the previous list element or nil.
func (e *Element[K, V]) Prev() *Element[K, V] {
return e.prev
}

// list represents a null terminated (non circular) intrusive doubly linked list.
// The list is immediately usable after instantiation without the need of a dedicated initialization.
type list[K comparable, V any] struct {
root Element[K, V] // list head and tail
}

func (l *list[K, V]) IsEmpty() bool {
return l.root.next == nil
}

// Front returns the first element of list l or nil if the list is empty.
func (l *list[K, V]) Front() *Element[K, V] {
return l.root.next
}

// Back returns the last element of list l or nil if the list is empty.
func (l *list[K, V]) Back() *Element[K, V] {
return l.root.prev
}

// Remove removes e from its list
func (l *list[K, V]) Remove(e *Element[K, V]) {
if e.prev == nil {
l.root.next = e.next
} else {
e.prev.next = e.next
}
if e.next == nil {
l.root.prev = e.prev
} else {
e.next.prev = e.prev
}
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
}

// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *list[K, V]) PushFront(key K, value V) *Element[K, V] {
e := &Element[K, V]{Key: key, Value: value}
if l.root.next == nil {
// It's the first element
l.root.next = e
l.root.prev = e
return e
}

e.next = l.root.next
l.root.next.prev = e
l.root.next = e
return e
}

// PushBack inserts a new element e with value v at the back of list l and returns e.
func (l *list[K, V]) PushBack(key K, value V) *Element[K, V] {
e := &Element[K, V]{Key: key, Value: value}
if l.root.prev == nil {
// It's the first element
l.root.next = e
l.root.prev = e
return e
}

e.prev = l.root.prev
l.root.prev.next = e
l.root.prev = e
return e
}
Loading

0 comments on commit cc923df

Please sign in to comment.