├── LICENSE ├── README.md ├── easy.go ├── from.go ├── from_test.go ├── go.mod ├── go.sum ├── iterator.go ├── iterator_test.go └── ranger.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Elad Gavra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lazy 2 | 3 | A lazy iterator for lazy programmers with emphasis on clarity, and decent performance. 4 | 5 | ### Pick Your Source 6 | ```go 7 | // {1} 8 | lazy.FromFunc(func() (int, bool) { 9 | return 1, false 10 | }) 11 | 12 | // 1, 1, ... 13 | lazy.Generate(func() int { 14 | return 1 15 | }) 16 | 17 | lazy.FromValues("a", "b") // {"a", "b"} 18 | lazy.FromSlice([]string{"a", "b", "c"}) // {"a", "b", "c"} 19 | 20 | lazy.To(10) // [0, 10) 21 | lazy.From(5) // [5, inf) 22 | lazy.From(1).To(3) // {1, 2} 23 | lazy.From(0).To(10).By(50) // {0, 50} 24 | ``` 25 | 26 | ### Chain Operations 27 | Chain operations pointer free, interface free and gluten free: 28 | ```go 29 | import "github.com/gavraz/lazy" 30 | 31 | it := lazy.Generate(func() int { 32 | return rand.Int() % 100 33 | }).Filter(func(x int) bool { 34 | return x%2 == 1 35 | }).Map(func(v int) int { 36 | return v * v 37 | }).Limit(10) 38 | 39 | for v, ok := it.Next(); ok; v, ok = it.Next() { 40 | fmt.Println(v) 41 | } 42 | // Output: 49 25 1 1 25 1 81 9 25 49 43 | ``` 44 | Current Operations: 45 | * Filter 46 | * Map 47 | * Paginate 48 | * Limit 49 | * Discard 50 | 51 | ### Iterate Easily 52 | ```go 53 | it = lazy.(...).Easy() 54 | for it.Next() { 55 | // it.Value() 56 | } 57 | ``` 58 | Or, turn into a slice: 59 | ```go 60 | s := it.Limit(10).Slice() 61 | fmt.Println(cap(s)) // A best effort is made to allocate the exact size: cap(s) = 10 62 | ``` -------------------------------------------------------------------------------- /easy.go: -------------------------------------------------------------------------------- 1 | package lazy 2 | 3 | type Easy[T any] struct { 4 | v T 5 | f Iterator[T] 6 | } 7 | 8 | func (ez *Easy[T]) Next() bool { 9 | v, ok := ez.f.f() 10 | if !ok { 11 | return false 12 | } 13 | 14 | ez.v = v 15 | return true 16 | } 17 | 18 | func (ez *Easy[T]) Value() T { 19 | return ez.v 20 | } 21 | -------------------------------------------------------------------------------- /from.go: -------------------------------------------------------------------------------- 1 | package lazy 2 | 3 | func none[T any]() (T, bool) { 4 | var v T 5 | return v, false 6 | } 7 | 8 | func FromFunc[T any](next func() (T, bool)) Iterator[T] { 9 | return Iterator[T]{f: next, size: unknown} 10 | } 11 | 12 | func Generate[T any](rnd func() T) Iterator[T] { 13 | return FromFunc(func() (T, bool) { 14 | return rnd(), true 15 | }) 16 | } 17 | 18 | func FromSlice[T any](data []T) Iterator[T] { 19 | i := 0 20 | it := FromFunc(func() (T, bool) { 21 | if i >= len(data) { 22 | return none[T]() 23 | } 24 | 25 | v := data[i] 26 | i++ 27 | return v, true 28 | }) 29 | it.size = len(data) 30 | return it 31 | } 32 | 33 | func FromValues[T any](v ...T) Iterator[T] { 34 | return FromSlice(v) 35 | } 36 | -------------------------------------------------------------------------------- /from_test.go: -------------------------------------------------------------------------------- 1 | package lazy 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func Test_Generate(t *testing.T) { 9 | k := 2 10 | g := Generate[int](func() int { 11 | if k > 0 { 12 | k-- 13 | return 1 14 | } 15 | 16 | return 2 17 | }).Easy() 18 | 19 | assert.True(t, g.Next()) 20 | assert.Equal(t, 1, g.Value()) 21 | assert.True(t, g.Next()) 22 | assert.Equal(t, 1, g.Value()) 23 | 24 | assert.True(t, g.Next()) 25 | assert.Equal(t, 2, g.Value()) 26 | assert.True(t, g.Next()) 27 | assert.Equal(t, 2, g.Value()) 28 | } 29 | 30 | func Test_FromSlice(t *testing.T) { 31 | slice := FromSlice([]int{}).Easy() 32 | assert.False(t, slice.Next()) 33 | } 34 | 35 | func Test_FromValues(t *testing.T) { 36 | vals := FromValues(1).Easy() 37 | assert.True(t, vals.Next()) 38 | assert.Equal(t, 1, vals.Value()) 39 | assert.False(t, vals.Next()) 40 | 41 | vals = FromValues(1, 3, 5).Easy() 42 | assert.True(t, vals.Next()) 43 | assert.Equal(t, 1, vals.Value()) 44 | assert.True(t, vals.Next()) 45 | assert.Equal(t, 3, vals.Value()) 46 | assert.True(t, vals.Next()) 47 | assert.Equal(t, 5, vals.Value()) 48 | assert.False(t, vals.Next()) 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gavraz/lazy 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/stretchr/testify v1.8.4 7 | golang.org/x/exp v0.0.0-20231127185646-65229373498e 8 | mtoohey.com/iter/v2 v2.0.1 9 | ) 10 | 11 | require ( 12 | github.com/barweiss/go-tuple v1.1.2 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/barweiss/go-tuple v1.0.2 h1:RFUKJf8BaALDgX4WIS1fgmAeEFLQo9h+n/QrbIQrdSo= 2 | github.com/barweiss/go-tuple v1.0.2/go.mod h1:oX3AtZyuGnCuK3B4PlUIfR9spxyJ42bgTSqCI9GkPB0= 3 | github.com/barweiss/go-tuple v1.1.2 h1:ul9tIW0LZ5w+Vk/Hi3X9z3JyqkD0yaVGZp+nNTLW2YE= 4 | github.com/barweiss/go-tuple v1.1.2/go.mod h1:SpoVilkI7ycNrIkQxcQfS1JG5A+R40sWwEUlPONlp3k= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 10 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 11 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 12 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 13 | golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= 14 | golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | mtoohey.com/iter/v2 v2.0.1 h1:CRfuFgaaoqC3dglsOsobB6iygxjxVY1DvkPiEZW+PLA= 20 | mtoohey.com/iter/v2 v2.0.1/go.mod h1:182CUhCGSQjQnj8oadn36bUsQxyb5Ki2V55E0w0T7tM= 21 | -------------------------------------------------------------------------------- /iterator.go: -------------------------------------------------------------------------------- 1 | package lazy 2 | 3 | const unknown = -1 4 | 5 | type Iterator[T any] struct { 6 | f func() (T, bool) 7 | size int 8 | } 9 | 10 | func (it Iterator[T]) Limit(limit int) Iterator[T] { 11 | i := 0 12 | limiter := FromFunc[T](func() (T, bool) { 13 | v, ok := it.f() 14 | if i >= limit || !ok { 15 | return none[T]() 16 | } 17 | 18 | i++ 19 | return v, true 20 | }) 21 | 22 | limiter.size = limit 23 | return limiter 24 | } 25 | 26 | func (it Iterator[T]) Filter(filter func(T) bool) Iterator[T] { 27 | return FromFunc(func() (T, bool) { 28 | for v, ok := it.f(); ok; v, ok = it.f() { 29 | if filter(v) { 30 | return v, true 31 | } 32 | } 33 | 34 | return none[T]() 35 | }) 36 | } 37 | 38 | func (it Iterator[T]) Map(m func(T) T) Iterator[T] { 39 | return Map(it, m) 40 | } 41 | 42 | func Map[T any, S any](it Iterator[T], m func(T) S) Iterator[S] { 43 | mp := FromFunc(func() (S, bool) { 44 | v, ok := it.f() 45 | if !ok { 46 | return none[S]() 47 | } 48 | 49 | return m(v), true 50 | }) 51 | mp.size = it.size 52 | return mp 53 | } 54 | 55 | func (it Iterator[T]) Discard(count int) Iterator[T] { 56 | i := 0 57 | return FromFunc(func() (T, bool) { 58 | v, ok := it.f() 59 | for ; i < count && ok; i++ { 60 | v, ok = it.f() 61 | } 62 | 63 | return v, ok 64 | }) 65 | } 66 | 67 | func (it Iterator[T]) Paginate(page, count int) Iterator[T] { 68 | skipped := it.Discard((page - 1) * count) 69 | return skipped.Limit(count) 70 | } 71 | 72 | func (it Iterator[T]) Next() (T, bool) { 73 | return it.f() 74 | } 75 | 76 | func (it Iterator[T]) Easy() *Easy[T] { 77 | return &Easy[T]{f: it} 78 | } 79 | 80 | func (it Iterator[T]) Slice() []T { 81 | var all []T 82 | 83 | if it.size != unknown { 84 | all = make([]T, 0, it.size) 85 | } 86 | 87 | v, ok := it.f() 88 | for ok { 89 | all = append(all, v) 90 | v, ok = it.f() 91 | } 92 | 93 | return all 94 | } 95 | -------------------------------------------------------------------------------- /iterator_test.go: -------------------------------------------------------------------------------- 1 | package lazy 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "slices" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func Test_Limit(t *testing.T) { 12 | ten := To(10) 13 | r := To(100).Limit(10) 14 | 15 | limited := r.Slice() 16 | assert.True(t, slices.Equal(ten.Slice(), limited)) 17 | 18 | // verify the optimization works with exact allocation, 16 otherwise 19 | assert.Equal(t, 10, cap(limited)) 20 | } 21 | 22 | func Test_Filter(t *testing.T) { 23 | even := To(10).Filter(func(x int) bool { 24 | return x%2 == 0 25 | }) 26 | d2 := Range(0, 10, 2) 27 | 28 | assert.True(t, slices.Equal(d2.Slice(), even.Slice())) 29 | } 30 | 31 | func Test_Discard(t *testing.T) { 32 | d := FromValues(1, 2, 3, 4, 5).Discard(3) 33 | assert.True(t, slices.Equal([]int{4, 5}, d.Slice())) 34 | 35 | d = FromValues(1, 2, 3).Discard(10) 36 | assert.True(t, slices.Equal([]int{}, d.Slice())) 37 | } 38 | 39 | func Test_Paginate(t *testing.T) { 40 | p := FromValues(1, 2, 3).Paginate(1, 3) 41 | assert.True(t, slices.Equal([]int{1, 2, 3}, p.Slice())) 42 | 43 | p = FromValues(1, 2, 3, 3, 4, 5, 6, 7).Paginate(2, 3) 44 | assert.True(t, slices.Equal([]int{3, 4, 5}, p.Slice())) 45 | } 46 | 47 | func Test_Map(t *testing.T) { 48 | m := Map(FromValues(1, 2, 3), func(x int) string { 49 | return fmt.Sprint(x) 50 | }) 51 | 52 | res := m.Slice() 53 | exp := []string{"1", "2", "3"} 54 | assert.Equal(t, len(exp), len(res)) 55 | for i, s := range exp { 56 | strings.Compare(s, res[i]) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ranger.go: -------------------------------------------------------------------------------- 1 | package lazy 2 | 3 | import ( 4 | "golang.org/x/exp/constraints" 5 | ) 6 | 7 | type Numeric interface { 8 | constraints.Integer | constraints.Float 9 | } 10 | 11 | // Range returns an iterator for a range [begin, begin+delta, ..., end). 12 | // Values are assumed to be non-negative. 13 | // Note: end=0 represents infinity. 14 | func Range[T Numeric](begin, end, delta T) Iterator[T] { 15 | curr := begin 16 | return FromFunc(func() (T, bool) { 17 | next := curr + delta 18 | if inf := end == 0; next > end && !inf { 19 | return none[T]() 20 | } 21 | v := curr 22 | curr = next 23 | return v, true 24 | }) 25 | } 26 | 27 | type Ranger[T Numeric] struct { 28 | Iterator[T] 29 | begin T 30 | end T 31 | delta T 32 | } 33 | 34 | func (r Ranger[T]) To(end T) Ranger[T] { 35 | r.end = end 36 | r.Iterator = Range[T](r.begin, r.end, r.delta) 37 | return r 38 | } 39 | 40 | func (r Ranger[T]) By(delta T) Iterator[T] { 41 | cloned := r 42 | cloned.delta = delta 43 | cloned.Iterator = Range[T](cloned.begin, cloned.end, cloned.delta) 44 | return cloned.Iterator 45 | } 46 | 47 | func From[T Numeric](begin T) Ranger[T] { 48 | r := Ranger[T]{begin: begin, end: 0, delta: 1} 49 | r.Iterator = Range[T](r.begin, r.end, r.delta) 50 | return r 51 | } 52 | 53 | // To returns an iterator for the range: [0, end) 54 | func To(end int) Iterator[int] { 55 | return Range[int](0, end, 1) 56 | } 57 | --------------------------------------------------------------------------------