Description
I propose to introduce package collection in the generic version.
Problem
Obviously, access to the container object necessarily involves an iterative algorithm. You can either write the traversal method to the container object (internal iterator) or not provide any traversal algorithm at all and let the people who use the container implement it themselves (external iterator). Both of these cases seem to solve the problem.
However, in the former case, the container is overloaded with functions, it is not only responsible for the maintenance of elements in its own "container" (add, delete, etc.), but also has to provide the interface to iterate over itself; and because of the problem of saving the iterated state, you cannot do multiple iterations over the same container object at the same time. The second way is less hassle, but the internal details of the container is exposed.
And the emergence of the iterator pattern, a good solution to the above two cases of disadvantages.
Before the go language had generics, we had a more limited means of uniformly abstracting all containers because the types of elements were diverse, either at the cost of performance or at the cost of size, but with the advent of generics, we now have the conditions that not only give application developers a uniform way of traversing them, but also give library developers the possibility of extending them.
By introducing the collection package and providing an iterator interface in the standard library, it can be used like database/sql for the benefit of countless go developers at a fraction of the cost.
Solution
I tried to design the iterator pattern in this repository using the generic feature of go:
https://github.com/kulics-works/gollection
I think it would be more useful to design iterators and related operations based on the following rules.
- Key Point 1
The next function of the iterator returns values and bool types.
The reason is that this pattern is quite common in go, and is more in line with go's convention of not introducing optional types in addition.
type Iterator[T any] interface {
Next() (T, bool)
}
- Key Point 2
Iterator should also implement Iterable.
The iterator itself is also iterable, which allows iterators to be used as iterables and allows a wider range of arguments to the operator.
The cost of iterator is just one more function that returns itself, which is very cost effective.
type Iterator[T any] interface {
Iter() Iterator[T]
Next() (T, bool)
}
type Iterable[T any] interface {
Iter() iterator[T]
}
The iterator function on intermediate operations should always accept an Iterable argument that returns an iterator, allowing for greater type compatibility.
func Map[T any, R any](transform func(T) R, it Iterable[T]) Iterator[R]
- Key Point 3
Iterator operator functions such as map and filter should always be designed to operate as the preceding operation, placing the Iterable as the following argument.
This way the operation code is closer to the function name and is more readable when used in a nested fashion.
func Filter[T any](predecate func(T) bool, it Iterable[T]) Iterator[T]
func Map[T any, R any](transform func(T) R, it Iterable[T]) Iterator[R]
func Sum[T number](it Iterable[T]) T
func main() {
Sum(Map(Square, Filter(TakeEvenNumbers, ListOf(1, 2, 3))))
}
- Key Point 4
Always use a for loop as the implementation solution for iterator termination operations.
Even though it would be clearer to use recursive code, for loops are clearly more usable.
func Reduce[T any, R any](initial R, operation func(R, T) R, it Iterable[T]) R {
var iter = it.Iter()
var result = initial
for v, ok := iter.Next(); ok; v, ok = iter.Next() {
result = operation(result, v)
}
return result
}
Here are the api's that I think are essential.
package collection
type Iterator[T any] interface {
Iter() Iterator[T]
Next() (T, bool)
}
type Iterable[T any] interface {
Iter() iterator[T]
}
// Convert a slice of any type to an iterator
func SliceIter[T any](source []T) Iterator[T]
// Convert a string to an iterator
func StringIter(source string) Iterator[byte]
type Pair[T any, R any] struct {
First T
Second R
}
// Returns an indexed iterator
func WithIndex[T any](it Iterable[T]) Iterator[Pair[int, T]]
func Map[T any, R any](transform func(T) R, it Iterable[T]) Iterator[R]
func Filter[T any](predecate func(T) bool, it Iterable[T]) Iterator[T]
// Returns an iterator that only iterates over the first specified number of elements
func Limit[T any](count int, it Iterable[T]) Iterator[T]
// Returns an iterator that skips the specified number of elements, the skipping is one-time.
func Skip[T any](count int, it Iterable[T]) Iterator[T]
// Returns an iterator that repeatedly skips the specified number of elements; the skipping is repetitive.
func Step[T any](count int, it Iterable[T]) Iterator[T]
// Returns a new iterator spliced by two Iterables
func Concat[T any](left Iterable[T], right Iterable[T]) Iterator[T]
type Number interface {
type int, int64, int32, int16, int8, uint64, uint32, uint16, uint8, float64, float32
}
func Sum[T Number](it Iterable[T]) T
func Product[T Number](it Iterable[T]) T
func Max[T Number](it Iterable[T]) T
func Min[T Number](it Iterable[T]) T
func Average[T Number](it Iterable[T]) T
func Count[T any](it Iterable[T]) int
func ForEach[T any](action func(T), it Iterable[T])
func AllMatch[T any](predicate func(T) bool, it Iterable[T]) bool
func NoneMatch[T any](predicate func(T) bool, it Iterable[T]) bool
func AnyMatch[T any](predicate func(T) bool, it Iterable[T]) bool
func First[T any](it Iterable[T]) (value T, ok bool)
func Last[T any](it Iterable[T]) (value T, ok bool)
func At[T any](index int, it Iterable[T]) (value T, ok bool)
// Aggregate from front to back into one result
func Reduce[T any, R any](initial R, operation func(R, T) R, it Iterable[T]) R
// Aggregate from back to front into one result
func Fold[T any, R any](initial R, operation func(T, R) R, it Iterable[T]) R
With the above api, it will be very convenient to operate on any container type.
These are all the ideas.