Skip to content

Commit cc923df

Browse files
authored
Support Go iterators (#46)
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
1 parent 378040e commit cc923df

File tree

7 files changed

+1379
-30
lines changed

7 files changed

+1379
-30
lines changed

.github/workflows/ci.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
pull_request:
66

77
jobs:
8-
unit-tests:
8+
unit-tests-v2:
99
strategy:
1010
matrix:
1111
go:
@@ -19,3 +19,12 @@ jobs:
1919
go-version: ${{ matrix.go }}
2020
- run: go test -v ./...
2121
- run: cd v2 && go test -v ./...
22+
23+
unit-tests-v3:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v3
27+
- uses: actions/setup-go@v3
28+
with:
29+
go-version: '^1.23'
30+
- run: cd v3 && go test -v ./...

README.md

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
# 🔃 github.com/elliotchance/orderedmap/v2 [![GoDoc](https://godoc.org/github.com/elliotchance/orderedmap/v2?status.svg)](https://godoc.org/github.com/elliotchance/orderedmap/v2)
1+
# 🔃 github.com/elliotchance/orderedmap/v3 [![GoDoc](https://godoc.org/github.com/elliotchance/orderedmap/v3?status.svg)](https://godoc.org/github.com/elliotchance/orderedmap/v3)
22

33
## Basic Usage
44

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

88
```go
9-
import "github.com/elliotchance/orderedmap/v2"
9+
import "github.com/elliotchance/orderedmap/v3"
1010

1111
func main() {
1212
m := orderedmap.NewOrderedMap[string, any]()
@@ -19,26 +19,55 @@ func main() {
1919
}
2020
```
2121

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

2931
## Iterating
3032

31-
Be careful using `Keys()` as it will create a copy of all of the keys so it's
32-
only suitable for a small number of items:
33+
The following methods all return
34+
[iterators](https://go.dev/doc/go1.23#iterators) that can be used to loop over
35+
elements in an ordered map:
36+
37+
- `AllFromFront()`
38+
- `AllFromBack()`
39+
- `Keys()`
40+
- `Values()`
3341

3442
```go
35-
for _, key := range m.Keys() {
36-
value, _:= m.Get(key)
43+
// Iterate through all elements from oldest to newest:
44+
for key, value := range m.AllFromFront() {
3745
fmt.Println(key, value)
3846
}
3947
```
4048

41-
For larger maps you should use `Front()` or `Back()` to iterate per element:
49+
Iterators are safe to use bidirectionally, and will return `nil` once it goes
50+
beyond the first or last item. If the map is changing while the iteration is
51+
in-flight it may produce unexpected behavior.
52+
53+
If you want to get a slice of the map keys or values, you can use the standard
54+
`slices.Collect` method with the iterator returned from `Keys()` or `Values()`:
55+
56+
```go
57+
fmt.Println(slices.Collect(m.Keys())
58+
// [A B C]
59+
```
60+
61+
Likewise, calling `maps.Collect` on the iterator returned from `AllFromFront()`
62+
will create a regular unordered map from the ordered one:
63+
64+
```go
65+
fmt.Println(maps.Collect(m.AllFromFront())
66+
// [A:1 B:2 C:3]
67+
```
68+
69+
If you don't want to use iterators, you can also manually loop over the elements
70+
using `Front()` or `Back()` with `Next()`:
4271

4372
```go
4473
// Iterate through all elements from oldest to newest:
@@ -51,23 +80,3 @@ for el := m.Back(); el != nil; el = el.Prev() {
5180
fmt.Println(el.Key, el.Value)
5281
}
5382
```
54-
55-
In case you're using Go 1.23, you can also [iterate with
56-
`range`](https://go.dev/doc/go1.23#iterators) by using `Iterator()` or
57-
`ReverseIterator()` methods:
58-
59-
```go
60-
for key, value := range m.Iterator() {
61-
fmt.Println(key, value)
62-
}
63-
64-
for key, value := range m.ReverseIterator() {
65-
fmt.Println(key, value)
66-
}
67-
```
68-
69-
The iterator is safe to use bidirectionally, and will return `nil` once it goes
70-
beyond the first or last item.
71-
72-
If the map is changing while the iteration is in-flight it may produce
73-
unexpected behavior.

v3/go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/elliotchance/orderedmap/v3
2+
3+
go 1.23.0
4+
5+
require github.com/stretchr/testify v1.7.1
6+
7+
require (
8+
github.com/davecgh/go-spew v1.1.0 // indirect
9+
github.com/pmezard/go-difflib v1.0.0 // indirect
10+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
11+
)

v3/go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6+
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
7+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
9+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
11+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

v3/list.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package orderedmap
2+
3+
// 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.
4+
type Element[K comparable, V any] struct {
5+
// Next and previous pointers in the doubly-linked list of elements.
6+
// To simplify the implementation, internally a list l is implemented
7+
// as a ring, such that &l.root is both the next element of the last
8+
// list element (l.Back()) and the previous element of the first list
9+
// element (l.Front()).
10+
next, prev *Element[K, V]
11+
12+
// The key that corresponds to this element in the ordered map.
13+
Key K
14+
15+
// The value stored with this element.
16+
Value V
17+
}
18+
19+
// Next returns the next list element or nil.
20+
func (e *Element[K, V]) Next() *Element[K, V] {
21+
return e.next
22+
}
23+
24+
// Prev returns the previous list element or nil.
25+
func (e *Element[K, V]) Prev() *Element[K, V] {
26+
return e.prev
27+
}
28+
29+
// list represents a null terminated (non circular) intrusive doubly linked list.
30+
// The list is immediately usable after instantiation without the need of a dedicated initialization.
31+
type list[K comparable, V any] struct {
32+
root Element[K, V] // list head and tail
33+
}
34+
35+
func (l *list[K, V]) IsEmpty() bool {
36+
return l.root.next == nil
37+
}
38+
39+
// Front returns the first element of list l or nil if the list is empty.
40+
func (l *list[K, V]) Front() *Element[K, V] {
41+
return l.root.next
42+
}
43+
44+
// Back returns the last element of list l or nil if the list is empty.
45+
func (l *list[K, V]) Back() *Element[K, V] {
46+
return l.root.prev
47+
}
48+
49+
// Remove removes e from its list
50+
func (l *list[K, V]) Remove(e *Element[K, V]) {
51+
if e.prev == nil {
52+
l.root.next = e.next
53+
} else {
54+
e.prev.next = e.next
55+
}
56+
if e.next == nil {
57+
l.root.prev = e.prev
58+
} else {
59+
e.next.prev = e.prev
60+
}
61+
e.next = nil // avoid memory leaks
62+
e.prev = nil // avoid memory leaks
63+
}
64+
65+
// PushFront inserts a new element e with value v at the front of list l and returns e.
66+
func (l *list[K, V]) PushFront(key K, value V) *Element[K, V] {
67+
e := &Element[K, V]{Key: key, Value: value}
68+
if l.root.next == nil {
69+
// It's the first element
70+
l.root.next = e
71+
l.root.prev = e
72+
return e
73+
}
74+
75+
e.next = l.root.next
76+
l.root.next.prev = e
77+
l.root.next = e
78+
return e
79+
}
80+
81+
// PushBack inserts a new element e with value v at the back of list l and returns e.
82+
func (l *list[K, V]) PushBack(key K, value V) *Element[K, V] {
83+
e := &Element[K, V]{Key: key, Value: value}
84+
if l.root.prev == nil {
85+
// It's the first element
86+
l.root.next = e
87+
l.root.prev = e
88+
return e
89+
}
90+
91+
e.prev = l.root.prev
92+
l.root.prev.next = e
93+
l.root.prev = e
94+
return e
95+
}

0 commit comments

Comments
 (0)