iter

package module
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 5 Imported by: 0

README

iter

PkgGoDev

Lazy, composable, type-safe pipelines for Go 1.27 iterators — the chill sibling of samber/lo, built on range-over-func.

iter.Of(1, 2, 3, 4, 5, 6).
    Filter(func(v int) bool { return v%2 == 0 }).
    Map(func(v int) int { return v * v }).
    Tap(func(v int) { log.Println("even square:", v) }).
    Collect()
// [4 16 36]

Nothing runs until the terminal method — and you are the driver. break, Take, done. Upstream stops working the instant you stop asking. No pull iterators, no Next(), no ceremony.

Why you'll like it

  • Actually reads well. Go 1.27's generic methods turn of(x).filter(f). map(g).collect() into a fluent chain instead of paren soup.
  • Lazy or get out. Every op is a range-over-func. Work happens only when — and for as long as — you ask for elements. No eager surprises, no double work.
  • Errors are just data. MapErr / FlatMapErr emit (result, error) pairs. Log them, drop them, or collect them. The stream never stops.
  • Zero dependencies. iter, slices, maps, cmp, errors. That's it. That's the whole party. No vendoring drama, no supply-chain anxiety.

Classic Go vs iter

Same logic. Two flavors. You pick. iter trades a little perf for code that reads the way you think — no cranking through index math or nesting.

Map + Filter + Take. First 10 squares of the evens up to 10,000:

// Classic Go
nums := make([]int, 0, 10)
for _, v := range input {
    if v%2 == 0 {
        nums = append(nums, v*v)
        if len(nums) == 10 {
            break
        }
    }
}

// iter
nums := iter.Of(input...).
    Filter(func(v int) bool { return v%2 == 0 }).
    Map(func(v int) int { return v * v }).
    Take(10).
    Collect()

Sum. Accumulate with a plain for vs iter.Sum:

// Classic Go
var total int
for _, v := range nums {
    total += v
}

// iter
total := iter.Sum(iter.Of(nums...))

Map with errors. Turn strings into ints, skipping the ones that fail:

// Classic Go
parsed := make([]int, 0, len(raw))
for _, s := range raw {
    if n, err := strconv.Atoi(s); err == nil {
        parsed = append(parsed, n)
    }
}

// iter
parsed := iter.Of(raw...).
    MapErr(strconv.Atoi).
    IgnoreErr().
    Collect()

Dedup. Keep the first occurrence of each key:

// Classic Go
seen := make(map[int]struct{})
out := make([]int, 0, len(users))
for _, u := range users {
    if _, ok := seen[u.id]; ok {
        continue
    }
    seen[u.id] = struct{}{}
    out = append(out, u.id)
}

// iter
out := iter.Of(users...).UniqBy(func(u user) int { return u.id }).Collect()

Reading is more direct, but it comes at a cost (measured below): every terminal carries a fixed allocation overhead from range-over-func and the closures. Know your hot path.

Install

go get github.com/lrweck/iter

Needs Go 1.27. Nothing else. Seriously.

Pipeline methods

Method What it does
Map(f) Apply f to every element
Filter(f) Keep elements where f is true
SkipErr(check) Keep elements where check returns nil, skip the rest
UniqBy(key) Keep the first element of each distinct key (any T)
FlatMap(f) Flatten the sequences produced by f
Take(n) / Drop(n) Keep / drop the first n elements
TakeWhile(p) / DropWhile(p) Keep / drop elements while p is true
Tap(f) Peek at every element, pass it through (Elixir's tap/2)
Enumerate() Attach an index → Seq2[int, T]
Zip(o) Pair up corresponding elements of two sequences

Seq2 (the output of Enumerate / Zip) shares the pipeline: Filter, Take, Drop, TakeWhile, DropWhile, Tap, plus Keys, Values, Collect, and ToMap.

When the element type needs a constraint a method can't apply, you get a package function — same vibe, shorter stack:

iter.Uniq(iter.Of(1, 2, 2, 1))                        // []int{1, 2}
iter.Of(users).UniqBy(func(u user) int { return u.id })

The tail of the pipeline:

vals := iter.Of(3, 1, 4, 1).Tap(func(v int) { log.Printf("%d", v) }).Collect() // []int
first, ok := iter.Of(9, 8).First()                          // 9, true
last, ok := iter.Of(9, 8).Last()                            // 8, true
total := iter.Sum(iter.Of(1, 2, 3))                         // 6
best, ok := iter.Max(iter.Of(3, 1, 4))                      // 4, true
pop, ok := iter.Of(users).MaxBy(func(u user) int { return u.age })
pairs := iter.ToMap(iter.Of("a", "b").Enumerate())          // map[int]string
buckets := iter.Of(vals).GroupBy(func(v int) string { return ... })
pick := iter.Of(vals).KeyBy(func(v int) string { return ... })

Generators:

iter.Range(2, 5, 1).Collect()                                                // []int{2, 3, 4}
iter.Range(5, 1, -1).Collect()                                               // []int{5, 4, 3, 2}
iter.Repeat(3, "x").Collect()                                                // []string{"x", "x", "x"}
iter.RepeatBy(3, func(i int) int { return i * i }).Collect()                 // []int{0, 1, 4}
iter.From(slices.Values([]string{"a", "b"}))                               // wrap any stdlib iterator
iter.Concat(iter.Of(1), iter.Of(2, 3)).Collect()                         // []int{1, 2, 3}
iter.Contains(iter.Of(3, 1, 4), 4)                                       // true
iter.ToMap(iter.FromMap(m))                                              // map round-trip
iter.FromKeys(m).Collect(), iter.FromValues(m).Collect()                 // the map's keys / values
for v := range iter.Of(1, 2, 3).Seq() { ... }                            // or range directly

Predicate terminals bail at the first decisive element:

iter.Of(1, 3, 4).Some(isEven)      // true, 4 never even gets looked at
iter.Of(2, 4).Every(isEven)        // true
iter.Of(1, 3).None(isEven)         // true
iter.Of(1, 3, 4).Find(isEven)      // 4, true
iter.Of(codes).CountBy(isEven)     // int

Bad input? Whatever, keep going

Fallible ops never kill the stream. Every element pairs with an outcome as a Result[V], and you own what failures mean — no if err != nil after every single line:

parsed := iter.Of("1", "junk", "2").MapErr(strconv.Atoi)

// log failures as they flow through
parsed.Tap(func(v int, err error) {
    if err != nil {
        log.Printf("bad line: %v", err)
    }
}).CollectErr()

// filter the failures out, keep the wins
good := parsed.IgnoreErr().Collect() // []int{1, 2}

// ... or grab values and every failure at once
values, err := parsed.CollectErr() // []int{1, 2}, errors.Join of all failures
if err != nil {
    log.Printf("%v", err)
}

Errors behave the Go way: errors.Is to compare, %w to wrap. Because of course they do — we're not animals.

Why it's shaped like that

Go won't let a generic method constrain its own receiver's type. Three things fall off the method set because of it:

  • Sum / Max / Min / Uniq need numeric, ordered, or comparable element types, so they're package functions. Their keyed counterparts — SumBy / MaxBy / MinBy / UniqBy / KeyBy / GroupBy — shove the constraint onto the key and work as methods for any T.
  • Chunk returns Seq[[]T], and the compiler chases T → []T → ... through the method set until it gives up.
  • Error recovery pins the pair slot to error, which generic Seq2 methods can't say — so there's a dedicated Result[V] type whose methods (Take, Filter, IgnoreErr, Errors, CollectErr, Tap) speak error fluently.

Zero values are safe: a Seq, Seq2, or Result declared but never initialized behaves as an empty sequence.

Benchmarks

Measured on an Intel Core i7-13700H, go test -bench with -benchmem and -count=3. Compare a declarative pipeline against the equivalent traditional loop.

Pipeline: Map + Filter + Take (10,000 items, take the first 10 evens and square them)
Approach ns/op B/op allocs/op
Traditional loop ~134 0 0
iter ~3.9k 2304 18

Lazy short-circuiting works — Take(10) stops early. The cost comes from the range-over-func closures, not from scanning the whole source.

Map (1,000 items, v*2)
Approach ns/op B/op allocs/op
Traditional loop ~3.8k 8192 1
iter.Map(...).Collect() ~19k 25336 17
Filter (1,000 items, evens)
Approach ns/op B/op allocs/op
Traditional loop ~4.7k 8192 1
iter.Filter(...).Collect() ~12k 8312 15
Sum (1,000 items)
Approach ns/op B/op allocs/op
Traditional loop ~0.6k 0 0
iter.Sum(...) ~1.8k 48 2
Map with errors (1,000 strings → int, all valid)
Approach ns/op B/op allocs/op
Traditional loop ~4.0k 8192 1
iter.MapErr(...).IgnoreErr().Collect() ~19k 25400 19
Terminals (fixed overhead per call)
Terminal ns/op B/op allocs/op
First() ~78 80 3
Count() ~1.7k 48 2
Some(isEven) (early stop) ~2.5k 72 3
The cost in one sentence

Every method call pays a fixed overhead (~48B/2 allocs per terminal, plus the closures of each pipeline layer). For small lists or one-off operations this is whatever, who cares. For hot loops over millions of items, the traditional loop ends up 5–30× faster. iter is about readability and composition — reach for a plain loop on the hot path when you need raw speed, and let iter carry the lines you'd rather not hand-crank.

License

MIT

Documentation

Overview

Package iter provides lazy, composable sequences over Go 1.27 iterators.

Values flow through a pipeline of generic methods, so lookup cost is paid only on the final terminal method:

iter.Of(1, 2, 3, 4).Filter(isEven).Map(square).Collect()

Range-over-func makes consumers the driver: breaking out of a `range` on a returned sequence stops upstream work immediately.

Fallible operations (MapErr, FlatMapErr) run every element and pair the outcome as a Result[U], a sequence of (result, error): (result, nil) on success, (zero, err) on failure. The error travels as data, so it can be logged with Tap, dropped with IgnoreErr, collected with Errors, or surfaced with CollectErr.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Contains

func Contains[T comparable](s Seq[T], v T) bool

Contains reports whether the sequence contains v. Like Uniq, it is a package function because comparable applies to the element type; predicate checks are covered by Some.

func Max

func Max[T cmp.Ordered](s Seq[T]) (T, bool)

Max returns the greatest element, or ok=false if the sequence is empty.

func Min

func Min[T cmp.Ordered](s Seq[T]) (T, bool)

Min returns the least element, or ok=false if the sequence is empty.

func Sum

func Sum[T Number](s Seq[T]) T

Sum adds all elements. The empty sequence sums to zero.

func ToMap

func ToMap[K comparable, V any](s Seq2[K, V]) map[K]V

ToMap eagerly collects a pair sequence into a map.

Types

type KeyValue added in v0.2.0

type KeyValue[K, V any] struct {
	K K
	V V
}

KeyValue is a single key/value pair, the element type of Seq2.Collect.

type Number

type Number interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
		~float32 | ~float64 |
		~complex64 | ~complex128
}

Number is a constraint for all builtin numeric types, including complex (which cannot be ordered, so Max/Min stay on cmp.Ordered). It cannot be a method receiver constraint, so summing lives here as a package function, as math/rand/v2 does with its intType.

type Result

type Result[V any] struct {
	// contains filtered or unexported fields
}

Result is a fallible sequence: pairs of (result, error), where success is (result, nil) and failure is (zero, err). The errors travel as data, so they can be inspected or dropped without stopping the pipeline.

It is a Seq2 with the second type argument pre-bound to error, which is what lets its methods express error semantics that generic Seq2 methods cannot (Go does not allow a method to constrain its receiver's type parameters).

func FromResult

func FromResult[V any](seq stditer.Seq2[V, error]) Result[V]

FromResult wraps a standard pair iterator as a fallible sequence.

func (Result[V]) CollectErr

func (s Result[V]) CollectErr() ([]V, error)

CollectErr eagerly evaluates the fallible sequence, returning every successful value and all failures joined into one error with errors.Join, or nil if nothing failed.

func (Result[V]) Count

func (s Result[V]) Count() int

Count returns the number of outcome pairs.

func (Result[V]) Errors

func (s Result[V]) Errors() Seq[error]

Errors yields the error of each failed pair, for logging or counting.

func (Result[V]) Filter

func (s Result[V]) Filter(f func(V, error) bool) Result[V]

Filter keeps the outcome pairs for which f returns true.

func (Result[V]) IgnoreErr

func (s Result[V]) IgnoreErr() Seq[V]

IgnoreErr drops the error pairs and yields only the successful values as a plain sequence from here on: the errors are skipped and the value is kept.

func (Result[V]) Seq

func (s Result[V]) Seq() stditer.Seq2[V, error]

Seq exposes the underlying standard pair iterator, for ranging directly:

for v, err := range res.Seq() { ... }

func (Result[V]) Take

func (s Result[V]) Take(n int) Result[V]

Take keeps at most n outcome pairs, stopping the upstream pull that early.

func (Result[V]) Tap

func (s Result[V]) Tap(f func(V, error)) Result[V]

Tap inspects every outcome as it flows through, then yields it unchanged. This is where fallible pipelines log their errors.

type Seq

type Seq[T any] struct {
	// contains filtered or unexported fields
}

Seq is a lazy sequence of values of type T.

func Chunk

func Chunk[T any](s Seq[T], n int) Seq[[]T]

Chunk groups the sequence into slices of at most n elements. It panics if n is not positive.

Chunk is a package function, not a method: a generic method returning Seq[[]T] instantiates the receiver's own type with []T and trips the compiler's instantiation-cycle check.

func Concat

func Concat[T any](seqs ...Seq[T]) Seq[T]

Concat concatenates the sequences in order.

func From

func From[T any](seq stditer.Seq[T]) Seq[T]

From wraps a standard iterator into a Seq.

func FromKeys

func FromKeys[K comparable, V any](m map[K]V) Seq[K]

FromKeys yields the keys of a map.

func FromValues

func FromValues[K comparable, V any](m map[K]V) Seq[V]

FromValues yields the values of a map.

func Of

func Of[T any](vals ...T) Seq[T]

Of builds a Seq from its arguments.

func Range

func Range(from, to, step int) Seq[int]

Range yields from up to, but not including, to, stepping by step. It panics if step is zero. If step has the wrong sign for the direction from from to to, it yields nothing.

func Repeat

func Repeat[T any](n int, v T) Seq[T]

Repeat yields n copies of v.

func RepeatBy

func RepeatBy[T any](n int, f func(int) T) Seq[T]

RepeatBy yields the values produced by f(0), f(1), ..., f(n-1).

func Uniq

func Uniq[T comparable](s Seq[T]) Seq[T]

Uniq keeps only the first occurrence of each equal element, deduplicating the sequence. Like Max/Min/Sum, it is a package function because its comparable constraint applies to the element type, which a method cannot re-restrict; the key-based variant UniqBy handles non-comparable elements as a method.

func (Seq[T]) Collect

func (s Seq[T]) Collect() []T

Collect eagerly evaluates the sequence into a slice.

func (Seq[T]) Count

func (s Seq[T]) Count() int

Count returns the number of elements.

func (Seq[T]) CountBy

func (s Seq[T]) CountBy(pred func(T) bool) int

CountBy counts the elements for which pred is true.

func (Seq[T]) Drop

func (s Seq[T]) Drop(n int) Seq[T]

Drop discards the first n elements.

func (Seq[T]) DropWhile

func (s Seq[T]) DropWhile(pred func(T) bool) Seq[T]

DropWhile discards elements from the start while pred is true.

func (Seq[T]) Enumerate

func (s Seq[T]) Enumerate() Seq2[int, T]

Enumerate pairs each element with its index.

func (Seq[T]) Every

func (s Seq[T]) Every(pred func(T) bool) bool

Every reports whether pred is true for every element, stopping early.

func (Seq[T]) Filter

func (s Seq[T]) Filter(f func(T) bool) Seq[T]

Filter keeps elements for which f returns true.

func (Seq[T]) Find

func (s Seq[T]) Find(pred func(T) bool) (T, bool)

Find returns the first element for which pred is true, or ok=false.

func (Seq[T]) First

func (s Seq[T]) First() (T, bool)

First returns the first element, or ok=false if the sequence is empty.

func (Seq[T]) FlatMap

func (s Seq[T]) FlatMap[U any](f func(T) Seq[U]) Seq[U]

FlatMap concatenates the sequences produced by f.

func (Seq[T]) FlatMapErr

func (s Seq[T]) FlatMapErr[U any](f func(T) (Seq[U], error)) Result[U]

FlatMapErr is FlatMap with an error source: each element produces either a run of (result, nil) pairs or a single (zero, err) pair.

func (Seq[T]) ForEach added in v0.2.0

func (s Seq[T]) ForEach(f func(T))

ForEach drives the sequence, calling f for every element.

func (Seq[T]) GroupBy added in v0.2.0

func (s Seq[T]) GroupBy[K comparable](f func(T) K) map[K][]T

GroupBy buckets elements by the key produced by f, returning a map of the buckets.

func (Seq[T]) KeyBy added in v0.2.0

func (s Seq[T]) KeyBy[K comparable](key func(T) K) map[K]T

KeyBy pivots a single element per key: the last element for each key wins.

func (Seq[T]) Last

func (s Seq[T]) Last() (T, bool)

Last returns the final element, or ok=false if the sequence is empty.

func (Seq[T]) Map

func (s Seq[T]) Map[U any](f func(T) U) Seq[U]

Map applies f to each element.

func (Seq[T]) MapErr

func (s Seq[T]) MapErr[U any](f func(T) (U, error)) Result[U]

MapErr applies f to each element, pairing every outcome: (result, nil) on success, (zero, err) on failure. The error pairs never stop the pipeline; handling them is up to the consumer (IgnoreErr, Errors, CollectErr).

func (Seq[T]) MaxBy added in v0.2.0

func (s Seq[T]) MaxBy[K cmp.Ordered](key func(T) K) (T, bool)

MaxBy returns the element with the greatest key, or ok=false if the sequence is empty.

func (Seq[T]) MinBy added in v0.2.0

func (s Seq[T]) MinBy[K cmp.Ordered](key func(T) K) (T, bool)

MinBy returns the element with the least key, or ok=false if the sequence is empty.

func (Seq[T]) None

func (s Seq[T]) None(pred func(T) bool) bool

None reports whether pred is false for every element.

func (Seq[T]) Reduce

func (s Seq[T]) Reduce[U any](init U, f func(U, T) U) U

Reduce folds the sequence into a single value starting from init.

func (Seq[T]) ReduceErr

func (s Seq[T]) ReduceErr[U any](init U, f func(U, T) (U, error)) (U, error)

ReduceErr is Reduce with a fallible step. It stops at the first error, returning the zero value of U and the error. The remaining elements are not processed.

func (Seq[T]) Seq

func (s Seq[T]) Seq() stditer.Seq[T]

Seq exposes the underlying standard iterator, for ranging directly:

for v := range seq.Seq() { ... }

func (Seq[T]) SkipErr

func (s Seq[T]) SkipErr(check func(T) error) Seq[T]

SkipErr keeps elements for which check returns nil, skipping the failures and moving on to the next one.

func (Seq[T]) Some

func (s Seq[T]) Some(pred func(T) bool) bool

Some reports whether pred is true for at least one element, stopping early.

func (Seq[T]) SumBy added in v0.2.0

func (s Seq[T]) SumBy[K Number](key func(T) K) K

SumBy sums the keys produced by key. The empty sequence sums to zero.

func (Seq[T]) Take

func (s Seq[T]) Take(n int) Seq[T]

Take keeps at most n elements.

func (Seq[T]) TakeWhile

func (s Seq[T]) TakeWhile(pred func(T) bool) Seq[T]

TakeWhile yields elements from the start while pred is true.

func (Seq[T]) Tap

func (s Seq[T]) Tap(f func(T)) Seq[T]

Tap inspects every element as it flows through, then yields it unchanged. It is the Go counterpart of Elixir's |> tap/2: a hook for logging and side effects in the middle of a pipeline.

func (Seq[T]) UniqBy added in v0.2.0

func (s Seq[T]) UniqBy[K comparable](key func(T) K) Seq[T]

UniqBy keeps only the first element for each distinct key produced by key.

func (Seq[T]) Zip

func (s Seq[T]) Zip[U any](o Seq[U]) Seq2[T, U]

Zip pairs each element with the corresponding element of o, stopping at the shorter sequence.

type Seq2

type Seq2[K, V any] struct {
	// contains filtered or unexported fields
}

Seq2 is a lazy sequence of key/value pairs.

func From2

func From2[K, V any](seq stditer.Seq2[K, V]) Seq2[K, V]

From2 wraps a standard pair iterator into a Seq2.

func FromMap

func FromMap[K comparable, V any](m map[K]V) Seq2[K, V]

FromMap wraps map iteration as a key/value Seq2 (order not defined).

func (Seq2[K, V]) Collect added in v0.2.0

func (s Seq2[K, V]) Collect() []KeyValue[K, V]

Collect eagerly evaluates the pair sequence into a slice of KeyValue pairs.

func (Seq2[K, V]) Count

func (s Seq2[K, V]) Count() int

Count returns the number of pairs.

func (Seq2[K, V]) Drop added in v0.2.0

func (s Seq2[K, V]) Drop(n int) Seq2[K, V]

Drop discards the first n pairs.

func (Seq2[K, V]) DropWhile added in v0.2.0

func (s Seq2[K, V]) DropWhile(pred func(K, V) bool) Seq2[K, V]

DropWhile discards pairs from the start while pred is true.

func (Seq2[K, V]) Filter added in v0.2.0

func (s Seq2[K, V]) Filter(f func(K, V) bool) Seq2[K, V]

Filter keeps the pairs for which f returns true.

func (Seq2[K, V]) Keys

func (s Seq2[K, V]) Keys() Seq[K]

Keys yields the keys of a pair sequence.

func (Seq2[K, V]) Seq

func (s Seq2[K, V]) Seq() stditer.Seq2[K, V]

Seq exposes the underlying standard pair iterator, for ranging directly:

for k, v := range pairs.Seq() { ... }

func (Seq2[K, V]) Take added in v0.2.0

func (s Seq2[K, V]) Take(n int) Seq2[K, V]

Take keeps at most n pairs.

func (Seq2[K, V]) TakeWhile added in v0.2.0

func (s Seq2[K, V]) TakeWhile(pred func(K, V) bool) Seq2[K, V]

TakeWhile yields pairs from the start while pred is true.

func (Seq2[K, V]) Tap

func (s Seq2[K, V]) Tap(f func(K, V)) Seq2[K, V]

Tap inspects every pair as it flows through, then yields it unchanged; see Seq.Tap.

func (Seq2[K, V]) Values

func (s Seq2[K, V]) Values() Seq[V]

Values yields the values of a pair sequence.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL