Skip to content

Commit

Permalink
feat(collections): add runnable iterators (#385)
Browse files Browse the repository at this point in the history
  • Loading branch information
plastikfan committed Dec 12, 2023
1 parent c6bcfcd commit 0d7bca4
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 22 deletions.
105 changes: 91 additions & 14 deletions collections/iterators.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ type Iterator[T any] interface {
Reset(entries []T)
}

func newForwardIt[T any](elements []T, zero T) *forwardIterator[T] {
// 📚 NB: it is not possible to obtain the type of a generic parameter at runtime
// using reflection. Generics in Go are a compile-time feature, and type information
// is generally not available at runtime due to the language's design principles.
// This is why we need the client to pass in the zero value manually.
//
return &forwardIterator[T]{
baseIterator: baseIterator[T]{
zero: zero,
container: elements,
current: -1,
},
}
}

// ForwardIt creates a forward iterator over a non empty slice. If the provided
// slice is empty, then a nil iterator is returned.
//
Expand All @@ -63,16 +78,15 @@ type Iterator[T any] interface {
// panic. If the collection contains structs, then pass in an empty struct
// as the nil value.
func ForwardIt[T any](elements []T, zero T) Iterator[T] {
// 📚 NB: it is not possible to obtain the type of a generic parameter at runtime
// using reflection. Generics in Go are a compile-time feature, and type information
// is generally not available at runtime due to the language's design principles.
// This is why we need the client to pass in the zero value manually.
//
return &forwardIterator[T]{
return newForwardIt[T](elements, zero)
}

func newReverseIt[T any](elements []T, zero T) *reverseIterator[T] {
return &reverseIterator[T]{
baseIterator: baseIterator[T]{
zero: zero,
container: elements,
current: -1,
current: len(elements),
},
}
}
Expand All @@ -81,13 +95,7 @@ func ForwardIt[T any](elements []T, zero T) Iterator[T] {
// slice is empty, then a nil iterator is returned. (NB: please remember to check
// for a nil interface correctly; see the helper function IsNil in utils).
func ReverseIt[T any](elements []T, zero T) Iterator[T] {
return &reverseIterator[T]{
baseIterator: baseIterator[T]{
zero: zero,
container: elements,
current: len(elements),
},
}
return newReverseIt[T](elements, zero)
}

type baseIterator[T any] struct {
Expand Down Expand Up @@ -184,3 +192,72 @@ func (i *reverseIterator[T]) Reset(entries []T) {
i.container = entries
i.current = len(i.container)
}

type (
// RunEach is a client defined function which is invoked for each
// element in the sequence.
RunEach[T any, R any] func(T) R

// RunWhile is a client defined function that denotes that the iteration
// can continue. When it returns false, iteration lapses.
//
RunWhile[T any, R any] func(T, R) bool

// RunnableIterator implements a looping mechanism for the Iterator. The
// runnable version is intended to make iteration just that little bit
// easier. The key feature of the runnable iterator is to be able to
// process a sequence while a certain condition holds true (defined,
// by the client using the while function.) The runnable iterator
// passes the return value for that particular item, to the while
// function, along with the item itself, so that it can define logic
// based on the return result. If the client always want to invoke
// all items in a sequence, then the better solution would be just to
// use a standard for/range statement, rather than use this iterator.
//
RunnableIterator[T any, R any] interface {
Iterator[T]

// RunAll invokes the predicate for every member of the sequence, while
// Valid and while both holds true.
RunAll(each RunEach[T, R], while RunWhile[T, R])
}
)

type forwardRunnableIterator[T any, R any] struct {
forwardIterator[T]
}

// RunAll
func (i *forwardRunnableIterator[T, R]) RunAll(each RunEach[T, R], while RunWhile[T, R]) {
runAll(i, each, while)
}

// ForwardRunIt creates a forward runnable iterator
func ForwardRunIt[T any, R any](elements []T, zero T) RunnableIterator[T, R] {
return &forwardRunnableIterator[T, R]{
forwardIterator: *newForwardIt(elements, zero),
}
}

type reverseRunnableIterator[T any, R any] struct {
reverseIterator[T]
}

func (i *reverseRunnableIterator[T, R]) RunAll(each RunEach[T, R], while RunWhile[T, R]) {
runAll(i, each, while)
}

// ReverseRunIt creates a reverse runnable iterator
func ReverseRunIt[T any, R any](elements []T, zero T) RunnableIterator[T, R] {
return &reverseRunnableIterator[T, R]{
reverseIterator: *newReverseIt(elements, zero),
}
}

func runAll[T any, R any](it RunnableIterator[T, R], each RunEach[T, R], while RunWhile[T, R]) {
for entry := it.Start(); it.Valid(); entry = it.Next() {
if !while(entry, each(entry)) {
break
}
}
}
150 changes: 142 additions & 8 deletions collections/iterators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package collections_test

import (
"fmt"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -47,49 +48,71 @@ func (e *record) song() string {
}

func getSleeveIt(forward bool, sequence []sleeve) collections.Iterator[sleeve] {
var zero sleeve

return lo.TernaryF(
forward,
func() collections.Iterator[sleeve] {
return collections.ForwardIt(sequence, nil)
return collections.ForwardIt(sequence, zero)
},
func() collections.Iterator[sleeve] {
return collections.ReverseIt(sequence, nil)
return collections.ReverseIt(sequence, zero)
},
)
}

func getSleeveRunIt(forward bool, sequence []sleeve) collections.RunnableIterator[sleeve, error] { // RunnableIterator
var zero sleeve

return lo.TernaryF(
forward,
func() collections.RunnableIterator[sleeve, error] {
return collections.ForwardRunIt[sleeve, error](sequence, zero)
},
func() collections.RunnableIterator[sleeve, error] {
return collections.ReverseRunIt[sleeve, error](sequence, zero)
},
)
}

func getRecordPtrIt(forward bool, sequence []*record) collections.Iterator[*record] {
var zero *record

return lo.TernaryF(
forward,
func() collections.Iterator[*record] {
return collections.ForwardIt(sequence, nil)
return collections.ForwardIt(sequence, zero)
},
func() collections.Iterator[*record] {
return collections.ReverseIt(sequence, nil)
return collections.ReverseIt(sequence, zero)
},
)
}

func getRecordsIt(forward bool, sequence []record) collections.Iterator[record] {
zero := record{}

return lo.TernaryF(
forward,
func() collections.Iterator[record] {
return collections.ForwardIt(sequence, record{})
return collections.ForwardIt(sequence, zero)
},
func() collections.Iterator[record] {
return collections.ReverseIt(sequence, record{})
return collections.ReverseIt(sequence, zero)
},
)
}

func getInt32It(forward bool, sequence []int32) collections.Iterator[int32] {
var zero int32

return lo.TernaryF(
forward,
func() collections.Iterator[int32] {
return collections.ForwardIt(sequence, int32(0))
return collections.ForwardIt(sequence, zero)
},
func() collections.Iterator[int32] {
return collections.ReverseIt(sequence, int32(0))
return collections.ReverseIt(sequence, zero)
},
)
}
Expand Down Expand Up @@ -581,6 +604,117 @@ var _ = Describe("Iterators", func() {
Expect(actual).To(HaveExactElements(expected))
})
})

})

Context("runnable", Ordered, func() {
var sleeves []sleeve

BeforeAll(func() {
sleeves = []sleeve{
&record{name: "07 - cinnamon girl"},
&record{name: "08 - how to disappear"},
&record{name: "09 - california"},
&record{name: "BONUS - 01"},
&record{name: "BONUS - 02"},
}
})

Context("forward", func() {
When("while condition is never invalidated", func() {
It("🧪 should: invoke each for all items in sequence", func() {
const (
expected = 5
forward = true
)

iterator := getSleeveRunIt(forward, sleeves)
actual := 0
each := func(_ sleeve) error {
actual++

return nil
}
while := func(_ sleeve, err error) bool {
return true
}

iterator.RunAll(each, while)
Expect(actual).To(Equal(expected))
})
})

When("while condition is invalidated before end of sequence", func() {
It("🧪 should: invoke each for item until while fails", func() {
const (
expected = 4
forward = true
)

iterator := getSleeveRunIt(forward, sleeves)
actual := 0
each := func(_ sleeve) error {
actual++

return nil
}
while := func(s sleeve, err error) bool {
return strings.HasPrefix(s.song(), "0")
}

iterator.RunAll(each, while)
Expect(actual).To(Equal(expected))
})
})
})

Context("reverse", Ordered, func() {
When("while condition is never invalidated", func() {
It("🧪 should: invoke each for all items in sequence", func() {
const (
expected = 5
forward = false
)

iterator := getSleeveRunIt(forward, sleeves)
actual := 0
each := func(_ sleeve) error {
actual++

return nil
}
while := func(_ sleeve, err error) bool {
return true
}

iterator.RunAll(each, while)
Expect(actual).To(Equal(expected))
})
})

When("while condition is invalidated before end of sequence", func() {
It("🧪 should: invoke each for item until while fails", func() {
const (
expected = 3
forward = false
)

iterator := getSleeveRunIt(forward, sleeves)
actual := 0
each := func(_ sleeve) error {
actual++

return nil
}
while := func(s sleeve, err error) bool {
return strings.HasPrefix(s.song(), "BONUS")
}

iterator.RunAll(each, while)
Expect(actual).To(Equal(expected))
})
})
})
})
})
})

0 comments on commit 0d7bca4

Please sign in to comment.