Documentation
¶
Overview ¶
Package catena is a lazy, fully-typed sequence library for Go 1.27+, built on iter.Seq and generic methods.
The four types:
- Seq[T]: lazy, single-pass by contract, possibly infinite.
- Seq2[K, V]: the stdlib pairing currency; a bridge back to Seq, not a peer.
- Try[T]: Seq2[T, error]; error policy is chosen by the consumer (Collect and UntilErr stop at the first error, CollectAll drains, Ignore skips).
- List[T]: eager []T with the mirrored operation set.
Finding an operator: most are methods, but operations that constrain the element type (Distinct, Sorted, Sum, Max, Contains, Union, …) are package functions, because a method on Seq[T any] may require nothing of T. So are Chunked, ChunkedBy and Windowed, which return Seq[[]T] — a method doing that is an instantiation cycle — and the Flatten family, which constrains the receiver's shape. Spelling is catena.Distinct(s), not s.Distinct(); the chain continues normally afterwards, as catena.Distinct(s).Filter(f) — or stays unbroken with Then, as s.Then(catena.Distinct).Filter(f). If the compiler reports "has no field or method Distinct", this is why.
Beside this package: catena/parallel has the operators that start goroutines, catena/source the producers for readers, paginated APIs and SQL rows.
Contracts every caller should know:
- Treat every Seq as single-pass. Re-iterability depends entirely on the producer (see the table on Seq); operators preserve it either way. Once() is a development guard.
- Nil sequences are empty: every method and package function accepts a nil receiver or argument and treats it as an empty sequence.
- Invalid construction arguments (negative counts, zero step) panic at construction time with a "catena: " prefixed message. Nil callbacks are not defended and panic at first use.
- Operators marked as buffering state their bound; terminals marked as full-drain hang on infinite input.
- Map-returning terminals return Go maps: iteration order is undefined. FoldedBy and GroupedBy are the ordered, chainable forms.
- When a Try element carries a non-nil error, do not read the value.
- Not safe for concurrent use. ToChan is the fan-out mechanism, and the only goroutine this package starts; package parallel has the rest.
- comparable-constrained functions panic at runtime if T is an interface type holding a non-comparable value (Go 1.20 semantics).
Example (LazyAcquisition) ¶
package main
import (
"fmt"
"io"
"strings"
"github.com/NerdMeNot/catena/source"
)
func main() {
opened := 0
logs := source.Lines(func() (io.ReadCloser, error) {
opened++
return io.NopCloser(strings.NewReader("GET /a\nPOST /b\nGET /c\nGET /d")), nil
})
// Building the pipeline opens nothing.
gets := logs.Ignore().
Filter(func(l string) bool { return strings.HasPrefix(l, "GET ") }).
Take(2)
fmt.Println("opened after building:", opened)
fmt.Println(gets.Collect())
fmt.Println("opened after consuming:", opened)
}
Output: opened after building: 0 [GET /a GET /c] opened after consuming: 1
Index ¶
- Constants
- func AssociateWith[T comparable, V any](s Seq[T], f func(T) V) map[T]V
- func Average[T Numeric](s Seq[T]) (float64, bool)
- func BottomN[T cmp.Ordered](s Seq[T], n int) []T
- func CollectMap[K comparable, V any](s Seq2[K, V]) map[K]V
- func Contains[T comparable](s Seq[T], v T) bool
- func Equal[T comparable](a, b Seq[T]) bool
- func IndexOf[T comparable](s Seq[T], v T) int
- func IsSorted[T cmp.Ordered](s Seq[T]) bool
- func Join(s Seq[string], sep string) string
- func Max[T cmp.Ordered](s Seq[T]) (T, bool)
- func Min[T cmp.Ordered](s Seq[T]) (T, bool)
- func MinMax[T cmp.Ordered](s Seq[T]) (min, max T, ok bool)
- func Product[T Numeric](s Seq[T]) T
- func Self[T any](v T) T
- func Sum[T Numeric](s Seq[T]) T
- func Tally[T comparable](s Seq[T]) map[T]int
- func ToKeySet[T comparable](s Seq[T]) map[T]struct{}
- func TopN[T cmp.Ordered](s Seq[T], n int) []T
- func Unzip[K, V any](s Seq2[K, V]) ([]K, []V)
- type Integer
- type List
- func (l List[T]) All(pred func(T) bool) bool
- func (l List[T]) Any(pred func(T) bool) bool
- func (l List[T]) Append(vals ...T) List[T]
- func (l List[T]) AsSeq() Seq[T]
- func (l List[T]) Associate[K comparable, V any](f func(T) (K, V)) map[K]V
- func (l List[T]) At(i int) T
- func (l List[T]) AverageOf[N Numeric](sel func(T) N) (float64, bool)
- func (l List[T]) BottomNBy[K cmp.Ordered](n int, sel func(T) K) []T
- func (l List[T]) Clone() List[T]
- func (l List[T]) Collect() []T
- func (l List[T]) Concat(others ...Seq[T]) List[T]
- func (l List[T]) Count() int
- func (l List[T]) CountWhere(pred func(T) bool) int
- func (l List[T]) DedupeBy[K comparable](sel func(T) K) List[T]
- func (l List[T]) DistinctBy[K comparable](sel func(T) K) List[T]
- func (l List[T]) DistinctWith(eq func(a, b T) bool) List[T]
- func (l List[T]) Drain()
- func (l List[T]) Drop(n int) List[T]
- func (l List[T]) DropLast(n int) List[T]
- func (l List[T]) DropWhile(pred func(T) bool) List[T]
- func (l List[T]) DuplicatesBy[K comparable](sel func(T) K) List[T]
- func (l List[T]) ElementAt(i int) (T, bool)
- func (l List[T]) Filter(pred func(T) bool) List[T]
- func (l List[T]) FilterErr(pred func(T) (bool, error)) Try[T]
- func (l List[T]) FilterIndexed(pred func(int, T) bool) List[T]
- func (l List[T]) FilterMap[U any](f func(T) (U, bool)) List[U]
- func (l List[T]) FilterNot(pred func(T) bool) List[T]
- func (l List[T]) Find(pred func(T) bool) (T, bool)
- func (l List[T]) FindIndex(pred func(T) bool) int
- func (l List[T]) FindLast(pred func(T) bool) (T, bool)
- func (l List[T]) FindMap[U any](f func(T) (U, bool)) (U, bool)
- func (l List[T]) First() (T, bool)
- func (l List[T]) FirstOr(fallback T) T
- func (l List[T]) FlatMap[U any](f func(T) Seq[U]) List[U]
- func (l List[T]) FlatMapSlice[U any](f func(T) []U) List[U]
- func (l List[T]) Fold[A any](init A, f func(A, T) A) A
- func (l List[T]) FoldBy[K comparable, A any](key func(T) K, init func(K) A, f func(A, T) A) map[K]A
- func (l List[T]) FoldErr[A any](init A, f func(A, T) (A, error)) (A, error)
- func (l List[T]) FoldIndexed[A any](init A, f func(int, A, T) A) A
- func (l List[T]) FoldRight[A any](init A, f func(T, A) A) A
- func (l List[T]) FoldWhile[A any](init A, f func(A, T) (A, bool)) A
- func (l List[T]) FoldedBy[K comparable, A any](key func(T) K, init func(K) A, f func(A, T) A) Seq2[K, A]
- func (l List[T]) ForEach(f func(T))
- func (l List[T]) ForEachErr(f func(T) error) error
- func (l List[T]) ForEachIndexed(f func(int, T))
- func (l List[T]) Get(i int) (T, bool)
- func (l List[T]) GroupBy[K comparable](sel func(T) K) map[K][]T
- func (l List[T]) GroupJoinBy[U any, K comparable, R any](other Seq[U], leftKey func(T) K, rightKey func(U) K, combine func(T, []U) R) List[R]
- func (l List[T]) IfEmpty(defaults ...T) List[T]
- func (l List[T]) IndexBy[K comparable](sel func(T) K) map[K]T
- func (l List[T]) Intersperse(sep T) List[T]
- func (l List[T]) IsEmpty() bool
- func (l List[T]) IsSortedBy[K cmp.Ordered](sel func(T) K) bool
- func (l List[T]) IsSortedWith(cmp func(a, b T) int) bool
- func (l List[T]) JoinBy[U any, K comparable, R any](other Seq[U], leftKey func(T) K, rightKey func(U) K, combine func(T, U) R) List[R]
- func (l List[T]) JoinToString(sep string, sel func(T) string) string
- func (l List[T]) Last() (T, bool)
- func (l List[T]) LastOr(fallback T) T
- func (l List[T]) LeftJoinBy[U any, K comparable, R any](other Seq[U], leftKey func(T) K, rightKey func(U) K, ...) List[R]
- func (l List[T]) Len() int
- func (l List[T]) Map[U any](f func(T) U) List[U]
- func (l List[T]) MapErr[U any](f func(T) (U, error)) Try[U]
- func (l List[T]) MapIndexed[U any](f func(int, T) U) List[U]
- func (l List[T]) MaxBy[K cmp.Ordered](sel func(T) K) (T, bool)
- func (l List[T]) MaxOf[K cmp.Ordered](sel func(T) K) (K, bool)
- func (l List[T]) MaxWith(cmp func(a, b T) int) (T, bool)
- func (l List[T]) MinBy[K cmp.Ordered](sel func(T) K) (T, bool)
- func (l List[T]) MinMaxOf[K cmp.Ordered](sel func(T) K) (min, max K, ok bool)
- func (l List[T]) MinOf[K cmp.Ordered](sel func(T) K) (K, bool)
- func (l List[T]) MinWith(cmp func(a, b T) int) (T, bool)
- func (l List[T]) None(pred func(T) bool) bool
- func (l List[T]) OnEach(f func(T)) List[T]
- func (l List[T]) Partition(pred func(T) bool) (yes, no []T)
- func (l List[T]) Prepend(vals ...T) List[T]
- func (l List[T]) ProductOf[N Numeric](sel func(T) N) N
- func (l List[T]) Reduce(f func(T, T) T) (T, bool)
- func (l List[T]) Reversed() List[T]
- func (l List[T]) Sample(n int, rng *rand.Rand) []T
- func (l List[T]) Scan[A any](init A, f func(A, T) A) List[A]
- func (l List[T]) Single() (T, bool)
- func (l List[T]) Slice(i, j int) List[T]
- func (l List[T]) SortedBy[K cmp.Ordered](sel func(T) K) List[T]
- func (l List[T]) SortedByDesc[K cmp.Ordered](sel func(T) K) List[T]
- func (l List[T]) SortedWith(cmp func(a, b T) int) List[T]
- func (l List[T]) Step(n int) List[T]
- func (l List[T]) SumOf[N Numeric](sel func(T) N) N
- func (l List[T]) Take(n int) List[T]
- func (l List[T]) TakeLast(n int) List[T]
- func (l List[T]) TakeWhile(pred func(T) bool) List[T]
- func (l List[T]) TallyBy[K comparable](sel func(T) K) map[K]int
- func (l List[T]) ToList() List[T]
- func (l List[T]) TopNBy[K cmp.Ordered](n int, sel func(T) K) []T
- func (l List[T]) WithIndex() Seq2[int, T]
- func (l List[T]) ZipWithNext() Seq2[T, T]
- type Numeric
- type Seq
- func Chain[T any](seqs ...Seq[T]) Seq[T]
- func Chunked[T any](s Seq[T], n int) Seq[[]T]
- func ChunkedBy[T any, K comparable](s Seq[T], sel func(T) K) Seq[[]T]
- func Cycle[T any](s Seq[T]) Seq[T]
- func Dedupe[T comparable](s Seq[T]) Seq[T]
- func Distinct[T comparable](s Seq[T]) Seq[T]
- func Duplicates[T comparable](s Seq[T]) Seq[T]
- func Empty[T any]() Seq[T]
- func Except[T comparable](a, b Seq[T]) Seq[T]
- func Flatten[T any](s Seq[Seq[T]]) Seq[T]
- func FlattenSlices[T any](s Seq[[]T]) Seq[T]
- func From[T any](seq func(func(T) bool)) Seq[T]
- func FromChan[T any](ctx context.Context, ch <-chan T) Seq[T]
- func FromSlice[T any](s []T) Seq[T]
- func Generate[T any](seed T, next func(T) T) Seq[T]
- func GenerateWhile[T any](seed T, next func(T) (T, bool)) Seq[T]
- func Intersect[T comparable](a, b Seq[T]) Seq[T]
- func NonZero[T comparable](s Seq[T]) Seq[T]
- func Of[T any](vals ...T) Seq[T]
- func Once1[T any](v T) Seq[T]
- func Range[I Integer](start, stop, step I) Seq[I]
- func Repeat[T any](v T) Seq[T]
- func RepeatN[T any](v T, n int) Seq[T]
- func Sorted[T cmp.Ordered](s Seq[T]) Seq[T]
- func SortedDesc[T cmp.Ordered](s Seq[T]) Seq[T]
- func Union[T comparable](a, b Seq[T]) Seq[T]
- func Windowed[T any](s Seq[T], size, step int) Seq[[]T]
- func (s Seq[T]) All(pred func(T) bool) bool
- func (s Seq[T]) Any(pred func(T) bool) bool
- func (s Seq[T]) Append(vals ...T) Seq[T]
- func (s Seq[T]) Associate[K comparable, V any](f func(T) (K, V)) map[K]V
- func (s Seq[T]) AverageOf[N Numeric](sel func(T) N) (float64, bool)
- func (s Seq[T]) BottomNBy[K cmp.Ordered](n int, sel func(T) K) []T
- func (s Seq[T]) Collect() []T
- func (s Seq[T]) Concat(others ...Seq[T]) Seq[T]
- func (s Seq[T]) Count() int
- func (s Seq[T]) CountWhere(pred func(T) bool) int
- func (s Seq[T]) DedupeBy[K comparable](sel func(T) K) Seq[T]
- func (s Seq[T]) DistinctBy[K comparable](sel func(T) K) Seq[T]
- func (s Seq[T]) DistinctWith(eq func(a, b T) bool) Seq[T]
- func (s Seq[T]) Drain()
- func (s Seq[T]) Drop(n int) Seq[T]
- func (s Seq[T]) DropLast(n int) Seq[T]
- func (s Seq[T]) DropWhile(pred func(T) bool) Seq[T]
- func (s Seq[T]) DuplicatesBy[K comparable](sel func(T) K) Seq[T]
- func (s Seq[T]) ElementAt(i int) (T, bool)
- func (s Seq[T]) Filter(pred func(T) bool) Seq[T]
- func (s Seq[T]) FilterErr(pred func(T) (bool, error)) Try[T]
- func (s Seq[T]) FilterIndexed(pred func(int, T) bool) Seq[T]
- func (s Seq[T]) FilterMap[U any](f func(T) (U, bool)) Seq[U]
- func (s Seq[T]) FilterNot(pred func(T) bool) Seq[T]
- func (s Seq[T]) Find(pred func(T) bool) (T, bool)
- func (s Seq[T]) FindIndex(pred func(T) bool) int
- func (s Seq[T]) FindLast(pred func(T) bool) (T, bool)
- func (s Seq[T]) FindMap[U any](f func(T) (U, bool)) (U, bool)
- func (s Seq[T]) First() (T, bool)
- func (s Seq[T]) FirstOr(fallback T) T
- func (s Seq[T]) FlatMap[U any](f func(T) Seq[U]) Seq[U]
- func (s Seq[T]) FlatMapSlice[U any](f func(T) []U) Seq[U]
- func (s Seq[T]) Fold[A any](init A, f func(A, T) A) A
- func (s Seq[T]) FoldBy[K comparable, A any](key func(T) K, init func(K) A, f func(A, T) A) map[K]A
- func (s Seq[T]) FoldErr[A any](init A, f func(A, T) (A, error)) (A, error)
- func (s Seq[T]) FoldIndexed[A any](init A, f func(int, A, T) A) A
- func (s Seq[T]) FoldWhile[A any](init A, f func(A, T) (A, bool)) A
- func (s Seq[T]) FoldedBy[K comparable, A any](key func(T) K, init func(K) A, f func(A, T) A) Seq2[K, A]
- func (s Seq[T]) ForEach(f func(T))
- func (s Seq[T]) ForEachErr(f func(T) error) error
- func (s Seq[T]) ForEachIndexed(f func(int, T))
- func (s Seq[T]) GroupBy[K comparable](sel func(T) K) map[K][]T
- func (s Seq[T]) GroupJoinBy[U any, K comparable, R any](other Seq[U], leftKey func(T) K, rightKey func(U) K, combine func(T, []U) R) Seq[R]
- func (s Seq[T]) IfEmpty(defaults ...T) Seq[T]
- func (s Seq[T]) IndexBy[K comparable](sel func(T) K) map[K]T
- func (s Seq[T]) Intersperse(sep T) Seq[T]
- func (s Seq[T]) IsEmpty() bool
- func (s Seq[T]) IsSortedBy[K cmp.Ordered](sel func(T) K) bool
- func (s Seq[T]) IsSortedWith(cmp func(a, b T) int) bool
- func (s Seq[T]) JoinBy[U any, K comparable, R any](other Seq[U], leftKey func(T) K, rightKey func(U) K, combine func(T, U) R) Seq[R]
- func (s Seq[T]) JoinToString(sep string, sel func(T) string) string
- func (s Seq[T]) Last() (T, bool)
- func (s Seq[T]) LastOr(fallback T) T
- func (s Seq[T]) LeftJoinBy[U any, K comparable, R any](other Seq[U], leftKey func(T) K, rightKey func(U) K, ...) Seq[R]
- func (s Seq[T]) Map[U any](f func(T) U) Seq[U]
- func (s Seq[T]) MapErr[U any](f func(T) (U, error)) Try[U]
- func (s Seq[T]) MapIndexed[U any](f func(int, T) U) Seq[U]
- func (s Seq[T]) MaxBy[K cmp.Ordered](sel func(T) K) (T, bool)
- func (s Seq[T]) MaxOf[K cmp.Ordered](sel func(T) K) (K, bool)
- func (s Seq[T]) MaxWith(cmp func(a, b T) int) (T, bool)
- func (s Seq[T]) MinBy[K cmp.Ordered](sel func(T) K) (T, bool)
- func (s Seq[T]) MinMaxOf[K cmp.Ordered](sel func(T) K) (min, max K, ok bool)
- func (s Seq[T]) MinOf[K cmp.Ordered](sel func(T) K) (K, bool)
- func (s Seq[T]) MinWith(cmp func(a, b T) int) (T, bool)
- func (s Seq[T]) None(pred func(T) bool) bool
- func (s Seq[T]) OnEach(f func(T)) Seq[T]
- func (s Seq[T]) Once() Seq[T]
- func (s Seq[T]) Partition(pred func(T) bool) (yes, no []T)
- func (s Seq[T]) Prepend(vals ...T) Seq[T]
- func (s Seq[T]) ProductOf[N Numeric](sel func(T) N) N
- func (s Seq[T]) Pull() (next func() (T, bool), stop func())
- func (s Seq[T]) Reduce(f func(T, T) T) (T, bool)
- func (s Seq[T]) Reversed() Seq[T]
- func (s Seq[T]) Sample(n int, rng *rand.Rand) []T
- func (s Seq[T]) Scan[A any](init A, f func(A, T) A) Seq[A]
- func (s Seq[T]) Seq() iter.Seq[T]
- func (s Seq[T]) Single() (T, bool)
- func (s Seq[T]) SortedBy[K cmp.Ordered](sel func(T) K) Seq[T]
- func (s Seq[T]) SortedByDesc[K cmp.Ordered](sel func(T) K) Seq[T]
- func (s Seq[T]) SortedWith(cmp func(a, b T) int) Seq[T]
- func (s Seq[T]) Step(n int) Seq[T]
- func (s Seq[T]) SumOf[N Numeric](sel func(T) N) N
- func (s Seq[T]) Take(n int) Seq[T]
- func (s Seq[T]) TakeLast(n int) Seq[T]
- func (s Seq[T]) TakeWhile(pred func(T) bool) Seq[T]
- func (s Seq[T]) TallyBy[K comparable](sel func(T) K) map[K]int
- func (s Seq[T]) Then[U any](f func(Seq[T]) U) U
- func (s Seq[T]) ToChan(ctx context.Context) <-chan T
- func (s Seq[T]) ToList() List[T]
- func (s Seq[T]) TopNBy[K cmp.Ordered](n int, sel func(T) K) []T
- func (s Seq[T]) UntilDone(ctx context.Context) Try[T]
- func (s Seq[T]) WithIndex() Seq2[int, T]
- func (s Seq[T]) Zip[U any](other Seq[U]) Seq2[T, U]
- func (s Seq[T]) ZipWithNext() Seq2[T, T]
- type Seq2
- func (s Seq2[K, V]) All(pred func(K, V) bool) bool
- func (s Seq2[K, V]) Any(pred func(K, V) bool) bool
- func (s Seq2[K, V]) Count() int
- func (s Seq2[K, V]) Drop(n int) Seq2[K, V]
- func (s Seq2[K, V]) Filter(pred func(K, V) bool) Seq2[K, V]
- func (s Seq2[K, V]) FilterNot(pred func(K, V) bool) Seq2[K, V]
- func (s Seq2[K, V]) First() (K, V, bool)
- func (s Seq2[K, V]) Fold[A any](init A, f func(A, K, V) A) A
- func (s Seq2[K, V]) ForEach(f func(K, V))
- func (s Seq2[K, V]) Keys() Seq[K]
- func (s Seq2[K, V]) Map[K2, V2 any](f func(K, V) (K2, V2)) Seq2[K2, V2]
- func (s Seq2[K, V]) MapTo[U any](f func(K, V) U) Seq[U]
- func (s Seq2[K, V]) MapValues[V2 any](f func(K, V) V2) Seq2[K, V2]
- func (s Seq2[K, V]) Pull() (next func() (K, V, bool), stop func())
- func (s Seq2[K, V]) Seq2() iter.Seq2[K, V]
- func (s Seq2[K, V]) Swap() Seq2[V, K]
- func (s Seq2[K, V]) Take(n int) Seq2[K, V]
- func (s Seq2[K, V]) Values() Seq[V]
- type Try
- func (t Try[T]) Collect() ([]T, error)
- func (t Try[T]) CollectAll() ([]T, []error)
- func (t Try[T]) Count() (int, error)
- func (t Try[T]) Drop(n int) Try[T]
- func (t Try[T]) Err() error
- func (t Try[T]) Errs() Seq[error]
- func (t Try[T]) Filter(pred func(T) bool) Try[T]
- func (t Try[T]) FilterErr(pred func(T) (bool, error)) Try[T]
- func (t Try[T]) FlatMap[U any](f func(T) Try[U]) Try[U]
- func (t Try[T]) Fold[A any](init A, f func(A, T) A) (A, error)
- func (t Try[T]) ForEach(f func(T) error) error
- func (t Try[T]) Ignore() Seq[T]
- func (t Try[T]) Map[U any](f func(T) U) Try[U]
- func (t Try[T]) MapErr[U any](f func(T) (U, error)) Try[U]
- func (t Try[T]) Must() Seq[T]
- func (t Try[T]) OnEach(f func(T)) Try[T]
- func (t Try[T]) OnError(f func(error)) Try[T]
- func (t Try[T]) Pull() (next func() (T, error, bool), stop func())
- func (t Try[T]) Recover(f func(error) (T, bool)) Try[T]
- func (t Try[T]) Seq2() iter.Seq2[T, error]
- func (t Try[T]) Take(n int) Try[T]
- func (t Try[T]) TakeWhile(pred func(T) bool) Try[T]
- func (t Try[T]) Then[U any](f func(Try[T]) U) U
- func (t Try[T]) UntilDone(ctx context.Context) Try[T]
- func (t Try[T]) UntilErr(err *error) Seq[T]
- func (t Try[T]) WrapErr(f func(error) error) Try[T]
Examples ¶
- Package (LazyAcquisition)
- AssociateWith
- Average
- BottomN
- Chain
- Chunked
- ChunkedBy
- CollectMap
- Contains
- Cycle
- Dedupe
- Distinct
- Duplicates
- Empty
- Empty2
- EmptyTry
- Equal
- Except
- Flatten
- FlattenSlices
- From
- From2
- FromChan
- FromErrs
- FromMap
- FromSlice
- Generate
- GenerateWhile
- GroupedBy
- IndexOf
- Intersect
- IsSorted
- Join
- List.Append
- List.AsSeq
- List.At
- List.Clone
- List.FoldRight
- List.Get
- List.Len
- List.Slice
- Max
- Min
- MinMax
- NonZero
- Of
- Once1
- Product
- Range
- Repeat
- RepeatN
- Self
- Seq2.All
- Seq2.Any
- Seq2.Count
- Seq2.Drop
- Seq2.Filter
- Seq2.FilterNot
- Seq2.First
- Seq2.Fold
- Seq2.ForEach
- Seq2.Keys
- Seq2.Map
- Seq2.MapTo
- Seq2.MapValues
- Seq2.Pull
- Seq2.Seq2
- Seq2.Swap
- Seq2.Take
- Seq2.Values
- Seq.All
- Seq.Any
- Seq.Append
- Seq.Associate
- Seq.AverageOf
- Seq.BottomNBy
- Seq.Collect
- Seq.Concat
- Seq.Count
- Seq.CountWhere
- Seq.DedupeBy
- Seq.DistinctBy
- Seq.DistinctWith
- Seq.Drain
- Seq.Drop
- Seq.DropLast
- Seq.DropWhile
- Seq.DuplicatesBy
- Seq.ElementAt
- Seq.Filter
- Seq.FilterErr
- Seq.FilterIndexed
- Seq.FilterMap
- Seq.FilterNot
- Seq.Find
- Seq.FindIndex
- Seq.FindLast
- Seq.FindMap
- Seq.First
- Seq.FirstOr
- Seq.FlatMap
- Seq.FlatMapSlice
- Seq.Fold
- Seq.FoldBy
- Seq.FoldErr
- Seq.FoldIndexed
- Seq.FoldWhile
- Seq.FoldedBy
- Seq.ForEach
- Seq.ForEachErr
- Seq.ForEachIndexed
- Seq.GroupBy
- Seq.GroupJoinBy
- Seq.IfEmpty
- Seq.IndexBy
- Seq.Intersperse
- Seq.IsEmpty
- Seq.IsSortedBy
- Seq.IsSortedWith
- Seq.JoinBy
- Seq.JoinToString
- Seq.Last
- Seq.LastOr
- Seq.LeftJoinBy
- Seq.Map
- Seq.MapErr
- Seq.MapIndexed
- Seq.MaxBy
- Seq.MaxOf
- Seq.MaxWith
- Seq.MinBy
- Seq.MinMaxOf
- Seq.MinOf
- Seq.MinWith
- Seq.None
- Seq.OnEach
- Seq.Once
- Seq.Partition
- Seq.Prepend
- Seq.ProductOf
- Seq.Pull
- Seq.Reduce
- Seq.Reversed
- Seq.Sample
- Seq.Scan
- Seq.Seq
- Seq.Single
- Seq.SortedBy
- Seq.SortedByDesc
- Seq.SortedWith
- Seq.Step
- Seq.SumOf
- Seq.Take
- Seq.TakeLast
- Seq.TakeWhile
- Seq.TallyBy
- Seq.Then
- Seq.ToChan
- Seq.ToList
- Seq.TopNBy
- Seq.UntilDone
- Seq.WithIndex
- Seq.Zip
- Seq.ZipWithNext
- Sorted
- SortedDesc
- Sum
- Tally
- ToKeySet
- TopN
- Try.Collect
- Try.CollectAll
- Try.Count
- Try.Drop
- Try.Err
- Try.Errs
- Try.Filter
- Try.FilterErr
- Try.FlatMap
- Try.Fold
- Try.ForEach
- Try.Ignore
- Try.Map
- Try.MapErr
- Try.Must
- Try.OnEach
- Try.OnError
- Try.Pull
- Try.Recover
- Try.Seq2
- Try.Take
- Try.TakeWhile
- Try.Then
- Try.UntilDone
- Try.UntilErr
- Try.WrapErr
- Union
- Unzip
- Windowed
Constants ¶
const Version = "1.6.0"
Version is the library's version, kept in lockstep with release tags: the release workflow refuses to publish a tag that disagrees with it.
Variables ¶
This section is empty.
Functions ¶
func AssociateWith ¶
func AssociateWith[T comparable, V any](s Seq[T], f func(T) V) map[T]V
AssociateWith maps each element to f(element); on duplicate elements the last value wins. ⚠ Full drain; map iteration order is undefined.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The elements become the keys; the function supplies each value.
hosts := catena.Of("web-1", "db-1")
probes := catena.AssociateWith(hosts, func(host string) string {
return "https://" + host + ".internal/healthz"
})
fmt.Println(probes["db-1"])
}
Output: https://db-1.internal/healthz
func Average ¶
Average returns the mean, accumulating in float64; (0, false) on empty input. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Always a float64, whatever the element type; false on empty.
ratings := catena.Of(5, 4, 4, 2)
mean, ok := catena.Average(ratings)
fmt.Println(mean, ok)
}
Output: 3.75 true
func BottomN ¶ added in v1.2.0
BottomN returns the n smallest elements, sorted ascending; equal elements retain encounter order. Memory is O(n) — the streaming alternative to Sorted(s).Take(n). ⚠ Full drain. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The two fastest laps, quickest first.
lapSecs := catena.Of(92.4, 89.7, 95.1, 90.2)
fmt.Println(catena.BottomN(lapSecs, 2))
}
Output: [89.7 90.2]
func CollectMap ¶
func CollectMap[K comparable, V any](s Seq2[K, V]) map[K]V
CollectMap drains a pair sequence into a map; on duplicate keys the last value wins. ⚠ Full drain; map iteration order is undefined.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// To a map. On a duplicate key the last value wins, as with plain map
// assignment — so a later setting overrides an earlier one.
names := catena.Of("port", "host", "port")
settings := catena.CollectMap(names.Zip(catena.Of("8080", "localhost", "9090")))
fmt.Println(settings)
}
Output: map[host:localhost port:9090]
func Contains ¶
func Contains[T comparable](s Seq[T], v T) bool
Contains reports whether v occurs in s; stops at the first match.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Membership by ==, stopping at the first hit.
roles := catena.Of("viewer", "editor", "admin")
fmt.Println(catena.Contains(roles, "admin"), catena.Contains(roles, "owner"))
}
Output: true false
func Equal ¶
func Equal[T comparable](a, b Seq[T]) bool
Equal reports whether a and b yield the same elements in the same order. Consumes both sequences up to and including the first difference — fully when they are equal. b is consumed through iter.Pull (its cleanup always runs).
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Same elements in the same order. A sequence that merely starts
// the same way is not equal.
expected := catena.Of("001_users", "002_orders", "003_indexes")
applied := catena.Of("001_users", "002_orders", "003_indexes")
behind := catena.Of("001_users", "002_orders")
fmt.Println(catena.Equal(expected, applied))
fmt.Println(catena.Equal(expected, behind))
}
Output: true false
func IndexOf ¶
func IndexOf[T comparable](s Seq[T], v T) int
IndexOf returns the index of the first occurrence of v; -1 if none.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The position of the first equal element, or -1.
stages := catena.Of("build", "test", "deploy")
fmt.Println(catena.IndexOf(stages, "deploy"))
fmt.Println(catena.IndexOf(stages, "rollback"))
}
Output: 2 -1
func IsSorted ¶ added in v1.5.0
IsSorted reports whether the elements are in ascending order; an empty or one-element sequence is sorted. NaN orders below everything (cmp.Compare), as it does for Sorted. It stops at the first pair out of order, so on infinite input it returns only if there is one.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// One pass, no buffer, and it stops at the first pair out of order.
// Equal neighbours count as sorted: two events in the same second are
// fine, a timestamp that goes backwards is not.
steady := catena.Of(1700000001, 1700000004, 1700000004, 1700000009)
skewed := catena.Of(1700000001, 1700000009, 1700000004)
fmt.Println(catena.IsSorted(steady), catena.IsSorted(skewed))
}
Output: true false
func Join ¶
Join concatenates a string sequence with sep between elements. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// strings.Join for a sequence, with no slice in between.
segments := catena.Of("api", "v2", "orders")
fmt.Println("/" + catena.Join(segments, "/"))
}
Output: /api/v2/orders
func Max ¶
Max returns the largest element; NaN orders below everything (cmp.Compare). ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The no-selector form of MaxOf, for values that order themselves.
latenciesMs := catena.Of(120, 85, 310, 96)
worst, ok := catena.Max(latenciesMs)
fmt.Println(worst, ok)
}
Output: 310 true
func Min ¶
Min returns the smallest element; NaN orders below everything, so a NaN in the input is the minimum. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// False on empty, since there is no smallest of nothing.
prices := catena.Of(12.50, 9.99, 14.00)
cheapest, ok := catena.Min(prices)
_, nonEmpty := catena.Min(catena.Empty[float64]())
fmt.Println(cheapest, ok, nonEmpty)
}
Output: 9.99 true false
func MinMax ¶
MinMax returns the smallest and largest elements in one pass. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The range of a day's readings in one pass rather than two.
tempsC := catena.Of(14.5, 21.0, 9.5, 17.0)
low, high, ok := catena.MinMax(tempsC)
fmt.Println(low, high, ok)
}
Output: 9.5 21 true
func Product ¶
Product multiplies the elements. Empty input yields 1, the multiplicative identity. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A parcel's volume from its sides, in cubic centimetres.
sidesCm := catena.Of(30, 20, 15)
fmt.Println(catena.Product(sidesCm))
}
Output: 9000
func Self ¶
func Self[T any](v T) T
Self is the identity selector: catena.Flatten(s) is s.FlatMap(Self).
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The identity selector, for the -By operators when the element is
// already the key: counting responses by status code.
statuses := catena.Of(200, 404, 200, 500, 200, 404)
fmt.Println(statuses.TallyBy(catena.Self[int]))
}
Output: map[200:3 404:2 500:1]
func Sum ¶
Sum adds the elements; integer overflow wraps like +. Empty input sums to 0. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Integer overflow wraps, exactly as + does.
downloadsPerDay := catena.Of(1_204, 980, 1_532)
fmt.Println(catena.Sum(downloadsPerDay))
}
Output: 3716
func Tally ¶
func Tally[T comparable](s Seq[T]) map[T]int
Tally counts occurrences per value. ⚠ Full drain; map iteration order is undefined.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// How many times each value occurs.
methods := catena.Of("GET", "POST", "GET", "GET", "DELETE")
counts := catena.Tally(methods)
fmt.Println(counts["GET"], counts["POST"], counts["PUT"])
}
Output: 3 1 0
func ToKeySet ¶ added in v1.1.0
func ToKeySet[T comparable](s Seq[T]) map[T]struct{}
ToKeySet drains the sequence into a membership map, whose keys are the distinct elements. Named for what it returns rather than for the set it stands in for: ToSet is reserved for a real set type, should Go ever grow one. ⚠ Full drain; map iteration order is undefined.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Duplicates collapse into a set built for membership tests.
blocked := catena.ToKeySet(catena.Of("10.0.0.7", "10.0.0.9", "10.0.0.7"))
_, denied := blocked["10.0.0.9"]
fmt.Println(len(blocked), denied)
}
Output: 2 true
func TopN ¶
TopN returns the n largest elements, sorted descending; equal elements retain encounter order. Memory is O(n) — the streaming alternative to SortedDesc(s).Take(n). ⚠ Full drain. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The podium, best first, without sorting the whole field.
scores := catena.Of(71, 94, 58, 88, 90)
fmt.Println(catena.TopN(scores, 3))
}
Output: [94 90 88]
func Unzip ¶
Unzip drains a pair sequence into its two sides; nil slices for empty input. ⚠ Full drain; buffers both sides.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Both sides in one pass — the safe way to get keys and values from
// a single-use Seq2. Here: which log lines failed, and what they said.
log := catena.Of("ok boot", "ERROR disk full", "ok sync", "ERROR timeout")
failures := log.WithIndex().
Filter(func(_ int, line string) bool { return strings.HasPrefix(line, "ERROR") })
lineNumbers, messages := catena.Unzip(failures)
fmt.Println(lineNumbers, messages)
}
Output: [1 3] [ERROR disk full ERROR timeout]
Types ¶
type Integer ¶
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
Integer is the constraint for Range.
type List ¶
type List[T any] []T
List is an eager []T with the mirrored method set. []T(l) unwraps it with no copy, and AsSeq() views it lazily for free.
The mirror covers Seq's methods, not the constraint-bound package functions: those take a Seq, so reach them through AsSeq and come back with ToList, as catena.Sorted(l.AsSeq()).ToList(). Concat likewise takes a Seq, so it is l1.Concat(l2.AsSeq()).
A few mirrored operators cross back to lazy, because their Seq counterparts return a lazy type: WithIndex, ZipWithNext and FoldedBy return Seq2, MapErr and FilterErr return Try.
func (List[T]) Append ¶
Append returns a new List with vals appended. Unlike the built-in append, the result ALWAYS has a fresh backing array and never aliases l — consistent with every other List transform. Callers who want the built-in's amortized behavior can use it directly: List is a []T.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Two commands built from one shared prefix. With the builtin append
// and spare capacity, the second would overwrite the first; here the
// result never aliases the receiver.
common := append(make(catena.List[string], 0, 8), "git", "log")
oneline := common.Append("--oneline")
graph := common.Append("--graph")
fmt.Println(oneline, graph)
}
Output: [git log --oneline] [git log --graph]
func (List[T]) AsSeq ¶
AsSeq returns a lazy, re-iterable view of the list. The list is not copied; mutations are visible to later iterations.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Crossing to lazy is explicit, and free — it is a view, not a copy.
// The search stops at the first match instead of testing every port.
candidates := catena.List[int]{80, 443, 8080, 8443, 9090}
port, ok := candidates.AsSeq().Find(func(p int) bool { return p >= 1024 })
fmt.Println(port, ok)
}
Output: 8080 true
func (List[T]) Associate ¶
func (l List[T]) Associate[K comparable, V any](f func(T) (K, V)) map[K]V
Associate mirrors Seq.Associate eagerly.
func (List[T]) At ¶
At returns the element at index i, panicking exactly like l[i] on an out-of-range index. O(1).
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Panics exactly like l[i] — it IS the index expression. Use it when
// the index is known to be valid.
stages := catena.List[string]{"build", "test", "deploy"}
fmt.Println(stages.At(stages.Len() - 1))
}
Output: deploy
func (List[T]) Clone ¶
Clone returns a shallow copy with a fresh backing array.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Start from shared defaults without changing them for everyone else.
defaults := catena.List[string]{"--color", "--jobs=4"}
mine := defaults.Clone()
mine[1] = "--jobs=16"
fmt.Println(defaults, mine)
}
Output: [--color --jobs=4] [--color --jobs=16]
func (List[T]) Collect ¶
func (l List[T]) Collect() []T
Collect mirrors Seq.Collect eagerly, implemented directly over the backing slice rather than through the lazy pipeline.
func (List[T]) Count ¶
Count mirrors Seq.Count eagerly, implemented directly over the backing slice rather than through the lazy pipeline.
func (List[T]) CountWhere ¶
CountWhere mirrors Seq.CountWhere eagerly.
func (List[T]) DedupeBy ¶
func (l List[T]) DedupeBy[K comparable](sel func(T) K) List[T]
DedupeBy mirrors Seq.DedupeBy eagerly.
func (List[T]) DistinctBy ¶
func (l List[T]) DistinctBy[K comparable](sel func(T) K) List[T]
DistinctBy mirrors Seq.DistinctBy eagerly.
func (List[T]) DistinctWith ¶
DistinctWith mirrors Seq.DistinctWith eagerly.
func (List[T]) DuplicatesBy ¶ added in v1.5.0
func (l List[T]) DuplicatesBy[K comparable](sel func(T) K) List[T]
DuplicatesBy mirrors Seq.DuplicatesBy eagerly.
func (List[T]) ElementAt ¶
ElementAt mirrors Seq.ElementAt eagerly, implemented directly over the backing slice rather than through the lazy pipeline.
func (List[T]) FilterErr ¶
FilterErr mirrors Seq.FilterErr. The list is consumed eagerly, but the result is a lazy Try — one of the List operators that cross back to lazy (see the List type).
func (List[T]) FilterIndexed ¶
FilterIndexed mirrors Seq.FilterIndexed eagerly.
func (List[T]) First ¶
First mirrors Seq.First eagerly, implemented directly over the backing slice rather than through the lazy pipeline.
func (List[T]) FirstOr ¶ added in v1.5.0
func (l List[T]) FirstOr(fallback T) T
FirstOr mirrors Seq.FirstOr eagerly.
func (List[T]) FlatMapSlice ¶
FlatMapSlice mirrors Seq.FlatMapSlice eagerly.
func (List[T]) FoldBy ¶
func (l List[T]) FoldBy[K comparable, A any](key func(T) K, init func(K) A, f func(A, T) A) map[K]A
FoldBy mirrors Seq.FoldBy eagerly.
func (List[T]) FoldIndexed ¶
FoldIndexed mirrors Seq.FoldIndexed eagerly.
func (List[T]) FoldRight ¶
FoldRight reduces right to left. It exists only on List: a right fold needs the whole sequence in memory, which a List already is.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Wrapping a handler in middleware: the last one listed has to wrap
// first so the first one listed ends up outermost. Exists only on
// List: a right fold needs the whole sequence, which a List already is.
middleware := catena.List[string]{"logging", "auth", "gzip"}
chain := middleware.FoldRight("handler", func(name, inner string) string {
return name + "(" + inner + ")"
})
fmt.Println(chain)
}
Output: logging(auth(gzip(handler)))
func (List[T]) FoldedBy ¶ added in v1.5.0
func (l List[T]) FoldedBy[K comparable, A any](key func(T) K, init func(K) A, f func(A, T) A) Seq2[K, A]
FoldedBy mirrors Seq.FoldedBy. The list is consumed eagerly, but the result is a lazy Seq2 — one of the List operators that cross back to lazy (see the List type).
func (List[T]) ForEachErr ¶
ForEachErr mirrors Seq.ForEachErr eagerly.
func (List[T]) ForEachIndexed ¶
ForEachIndexed mirrors Seq.ForEachIndexed eagerly.
func (List[T]) Get ¶
Get returns the element at index i; (zero, false) for a negative or out-of-range index. O(1).
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The comma-ok form, for when the index may be out of range: an
// optional command-line argument with a default.
args := catena.List[string]{"deploy"}
command, _ := args.Get(0)
target, ok := args.Get(1)
if !ok {
target = "staging"
}
fmt.Println(command, target)
}
Output: deploy staging
func (List[T]) GroupBy ¶
func (l List[T]) GroupBy[K comparable](sel func(T) K) map[K][]T
GroupBy mirrors Seq.GroupBy eagerly.
func (List[T]) GroupJoinBy ¶ added in v1.5.0
func (l List[T]) GroupJoinBy[U any, K comparable, R any](other Seq[U], leftKey func(T) K, rightKey func(U) K, combine func(T, []U) R) List[R]
GroupJoinBy mirrors Seq.GroupJoinBy eagerly.
func (List[T]) IndexBy ¶
func (l List[T]) IndexBy[K comparable](sel func(T) K) map[K]T
IndexBy mirrors Seq.IndexBy eagerly.
func (List[T]) Intersperse ¶
Intersperse mirrors Seq.Intersperse eagerly.
func (List[T]) IsEmpty ¶
IsEmpty mirrors Seq.IsEmpty eagerly, implemented directly over the backing slice rather than through the lazy pipeline.
func (List[T]) IsSortedBy ¶ added in v1.5.0
IsSortedBy mirrors Seq.IsSortedBy eagerly.
func (List[T]) IsSortedWith ¶ added in v1.5.0
IsSortedWith mirrors Seq.IsSortedWith eagerly.
func (List[T]) JoinBy ¶
func (l List[T]) JoinBy[U any, K comparable, R any](other Seq[U], leftKey func(T) K, rightKey func(U) K, combine func(T, U) R) List[R]
JoinBy mirrors Seq.JoinBy eagerly.
func (List[T]) JoinToString ¶
JoinToString mirrors Seq.JoinToString eagerly.
func (List[T]) Last ¶
Last mirrors Seq.Last eagerly, implemented directly over the backing slice rather than through the lazy pipeline.
func (List[T]) LastOr ¶ added in v1.5.0
func (l List[T]) LastOr(fallback T) T
LastOr mirrors Seq.LastOr eagerly.
func (List[T]) LeftJoinBy ¶ added in v1.5.0
func (l List[T]) LeftJoinBy[U any, K comparable, R any](other Seq[U], leftKey func(T) K, rightKey func(U) K, combine func(T, U, bool) R) List[R]
LeftJoinBy mirrors Seq.LeftJoinBy eagerly.
func (List[T]) Len ¶
Len returns the number of elements. O(1).
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A List is a slice, so its length is known without consuming
// anything — unlike Count on a lazy sequence.
replicas := catena.List[string]{"db-1", "db-2", "db-3"}
fmt.Println("quorum:", replicas.Len()/2+1)
}
Output: quorum: 2
func (List[T]) Map ¶
Map mirrors Seq.Map eagerly, implemented directly over the backing slice rather than through the lazy pipeline.
func (List[T]) MapErr ¶
MapErr mirrors Seq.MapErr. The list is consumed eagerly, but the result is a lazy Try — one of the List operators that cross back to lazy (see the List type).
func (List[T]) MapIndexed ¶
MapIndexed mirrors Seq.MapIndexed eagerly, implemented directly over the backing slice rather than through the lazy pipeline.
func (List[T]) Reversed ¶
Reversed mirrors Seq.Reversed eagerly, implemented directly over the backing slice rather than through the lazy pipeline.
func (List[T]) Slice ¶
Slice returns l[i:j] as a List, panicking exactly like the slice expression — and, exactly like it, sharing the backing array.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// One page of results. It is the slice expression, aliasing included:
// the page shares memory with the list it came from.
results := catena.List[string]{"ada", "grace", "linus", "ken", "dennis"}
page, size := 1, 2
fmt.Println(results.Slice(page*size, (page+1)*size))
}
Output: [linus ken]
func (List[T]) SortedByDesc ¶
SortedByDesc mirrors Seq.SortedByDesc eagerly.
func (List[T]) SortedWith ¶
SortedWith mirrors Seq.SortedWith eagerly.
func (List[T]) TallyBy ¶
func (l List[T]) TallyBy[K comparable](sel func(T) K) map[K]int
TallyBy mirrors Seq.TallyBy eagerly.
func (List[T]) ToList ¶
ToList mirrors Seq.ToList eagerly, implemented directly over the backing slice rather than through the lazy pipeline.
func (List[T]) WithIndex ¶
WithIndex mirrors Seq.WithIndex. The list is consumed eagerly, but the result is a lazy Seq2 — one of the List operators that cross back to lazy (see the List type).
func (List[T]) ZipWithNext ¶
ZipWithNext mirrors Seq.ZipWithNext. The list is consumed eagerly, but the result is a lazy Seq2 — one of the List operators that cross back to lazy (see the List type).
type Numeric ¶
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64
}
Numeric is the constraint for arithmetic aggregations (Sum, Product, Average). Complex types are deliberately excluded: they are unordered and half the aggregation surface would be meaningless for them.
type Seq ¶
Seq is a lazy sequence: iter.Seq with methods. Range over it directly.
A Seq is a function, so consuming it twice runs its producer twice. Whether that works is a property of the producer alone — operators never change it. Every operator builds its state inside the returned closure, so a chain is re-iterable exactly when its root producer is, and the conformance suite re-iterates every operator to prove it. Once() is the single deliberate exception.
Re-iterable Of, FromSlice, FromMap, Empty, Empty2, EmptyTry, Once1,
Repeat, RepeatN, Range, Generate and GenerateWhile (iff
next is pure)
Single-use FromChan, and From, From2, FromErrs when the underlying
source is (anything over I/O)
Cycle re-iterable iff its source's first pass is
Consuming a single-use sequence a second time yields nothing rather than failing: wrap it in Once() during development to turn that into a panic. Printing a Seq shows a function pointer — print s.Collect() instead.
func Chain ¶
Chain yields each sequence's elements in order. It is the package-function form of Seq.Concat, for when you hold a slice of sequences and no receiver.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The variadic form of Concat, for when the sequences are in a slice
// already and none of them is the obvious receiver — one per rotated
// log file, say.
rotated := []catena.Seq[string]{
catena.Of("mon:start", "mon:stop"),
catena.Of("tue:start"),
catena.Of("wed:start", "wed:crash"),
}
fmt.Println(catena.Chain(rotated...).Collect())
}
Output: [mon:start mon:stop tue:start wed:start wed:crash]
func Chunked ¶
Chunked yields consecutive chunks of n elements; the last chunk may be partial. Every chunk is a fresh slice (safe to retain). Panics if n <= 0.
Chunked is a package function, not a method: a method on Seq[T] returning Seq[[]T] is an instantiation cycle (each Seq[T] would require Seq[[]T], which would require Seq[[][]T], forever).
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Fixed-size batches, for an API that takes so many IDs per request;
// the last one may be short. Every chunk is a fresh slice, so keeping
// one is safe.
userIDs := catena.Range(101, 108, 1)
for batch := range catena.Chunked(userIDs, 3).Seq() {
fmt.Println("DELETE /users", batch)
}
}
Output: DELETE /users [101 102 103] DELETE /users [104 105 106] DELETE /users [107]
func ChunkedBy ¶
func ChunkedBy[T any, K comparable](s Seq[T], sel func(T) K) Seq[[]T]
ChunkedBy yields runs of consecutive elements sharing a key: a chunk closes when the key changes. Memory is bounded by the longest run. Every chunk is a fresh slice. A package function for the same instantiation- cycle reason as Chunked.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A chunk per run of equal keys — the streaming answer to grouping
// input that is already ordered, in memory bounded by the longest run.
// It splits on change, not on value: a host that reappears later
// starts a new chunk.
type entry struct {
Host string
Path string
}
access := catena.Of(
entry{"web-1", "/"}, entry{"web-1", "/cart"},
entry{"web-2", "/"},
entry{"web-1", "/pay"},
)
for run := range catena.ChunkedBy(access, func(e entry) string { return e.Host }).Seq() {
fmt.Println(run[0].Host, len(run))
}
}
Output: web-1 2 web-2 1 web-1 1
func Cycle ¶
Cycle yields s over and over, forever. The first pass is buffered and replayed (⚠ unbounded memory in len(s)), so Cycle is re-iterable iff that first pass of s is. An empty s yields an empty Cycle — it terminates rather than spinning.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Round-robin: Zip stops when the tickets run out, so the endless
// rota is never a problem. Over an empty source Cycle terminates
// rather than spinning.
tickets := catena.Of("T-101", "T-102", "T-103", "T-104", "T-105")
rota := catena.Cycle(catena.Of("ada", "bob"))
fmt.Println(tickets.Zip(rota).
MapTo(func(ticket, engineer string) string { return ticket + ":" + engineer }).
Collect())
fmt.Println(catena.Cycle(catena.Empty[string]()).Collect())
}
Output: [T-101:ada T-102:bob T-103:ada T-104:bob T-105:ada] []
func Dedupe ¶
func Dedupe[T comparable](s Seq[T]) Seq[T]
Dedupe yields elements that differ from their predecessor — consecutive duplicates only, O(1) memory.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Like uniq(1): consecutive duplicates only, so a repeat that comes
// back later is kept. On sorted input it equals Distinct at a
// fraction of the cost; on unsorted input the two differ.
log := catena.Of(
"connection reset", "connection reset", "connection reset",
"reconnected",
"connection reset",
)
fmt.Println(catena.Dedupe(log).JoinToString(" | ", catena.Self[string]))
}
Output: connection reset | reconnected | connection reset
func Distinct ¶
func Distinct[T comparable](s Seq[T]) Seq[T]
Distinct yields elements not seen before; the first occurrence wins.
Distinct is a package function, not a method, and so are the others in this file: they constrain the element type (comparable, cmp.Ordered, Numeric), and a method on Seq[T any] may require nothing of T — or, for the Flatten family, they constrain the receiver's shape. Every comparable-constrained function panics at runtime if T is an interface type holding a non-comparable value. The chain continues normally after them: catena.Distinct(s).Filter(f).
On input already sorted by the compared value, Dedupe is the O(1)-memory equivalent. ⚠ Retains one entry per distinct value — unbounded.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The unique client addresses in an access log, in the order they
// first appeared. Distinct constrains the element type, so it is a
// package function rather than a method — a method on Seq[T any] may
// require nothing of T.
clients := catena.Of("10.0.0.7", "10.0.0.9", "10.0.0.7", "10.0.0.4", "10.0.0.9")
fmt.Println(catena.Distinct(clients).Collect())
}
Output: [10.0.0.7 10.0.0.9 10.0.0.4]
func Duplicates ¶ added in v1.5.0
func Duplicates[T comparable](s Seq[T]) Seq[T]
Duplicates yields the elements that occur more than once: each such value once, at its second occurrence, so in the order values were first repeated. ⚠ Retains one entry per distinct value — unbounded.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Each value that occurs more than once, reported once however often
// it repeats — in the order the repeats were found.
requestIDs := catena.Of("r-71", "r-30", "r-92", "r-30", "r-71", "r-71")
fmt.Println(catena.Duplicates(requestIDs).Collect())
}
Output: [r-30 r-71]
func Empty ¶
Empty returns the empty Seq. Re-iterable.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The "nothing to report" return value: callers keep chaining on it
// without a nil check.
ordersFor := func(customer string) catena.Seq[string] {
if customer == "" {
return catena.Empty[string]()
}
return catena.Of("A-1001", "A-1002")
}
fmt.Println(ordersFor("").Count(), ordersFor("ada").Count())
}
Output: 0 2
func Except ¶
func Except[T comparable](a, b Seq[T]) Seq[T]
Except yields the distinct elements of a that do not occur in b, in a's encounter order. ⚠ Buffers all of b before a is consumed, plus a seen- set of a's distinct values.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// What the first side has that the second does not: the hosts still
// waiting for a patch.
fleet := catena.Of("web-1", "web-2", "db-1", "cache-1")
patched := catena.Of("web-2", "cache-1")
fmt.Println(catena.Except(fleet, patched).Collect())
}
Output: [web-1 db-1]
func Flatten ¶
Flatten yields every element of every inner sequence, in order.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// One stream from a sequence of sequences — here, the hosts of each
// region — in order, and still lazy.
byRegion := catena.Of(
catena.Of("eu-1.example.com", "eu-2.example.com"),
catena.Of("us-1.example.com"),
)
fmt.Println(catena.Flatten(byRegion).Collect())
}
Output: [eu-1.example.com eu-2.example.com us-1.example.com]
func FlattenSlices ¶
FlattenSlices yields every element of every slice, in order.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The same for slices: a paginated API hands back a page at a time,
// and the caller wants the rows.
pages := catena.Of(
[]string{"invoice-101", "invoice-102"},
[]string{"invoice-103"},
)
fmt.Println(catena.FlattenSlices(pages).Collect())
}
Output: [invoice-101 invoice-102 invoice-103]
func From ¶
From adapts any push-function sequence — iter.Seq, catena.Seq, or a third-party alias — with no conversion at the call site. Re-iterability depends on the source.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Takes the literal function type, so any iterator adapts without a
// conversion at the call site — including the standard library's.
path := "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin"
fmt.Println(catena.From(strings.SplitSeq(path, ":")).
Filter(func(dir string) bool { return strings.HasPrefix(dir, "/usr/") }).
Collect())
}
Output: [/usr/local/bin /usr/bin]
func FromChan ¶
FromChan yields values received from ch until ch is closed or ctx is done. Single-use. No goroutine is started; a sequence that is never consumed never receives.
Example ¶
package main
import (
"context"
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
results := make(chan string, 3)
for _, name := range []string{"a.png", "b.png", "c.png"} {
results <- "resized " + name
}
close(results)
// Drains a worker pool's results until the channel closes or ctx is
// done. Single-use, and it starts no goroutine: a sequence that is
// never consumed never receives.
fmt.Println(catena.FromChan(context.Background(), results).Collect())
}
Output: [resized a.png resized b.png resized c.png]
func FromSlice ¶
FromSlice returns a re-iterable Seq over s. The slice is not copied; mutations to it are visible to later iterations.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The slice is not copied, which is what makes this free — and makes
// the pipeline a live view: finish a job and the next iteration sees
// it.
type job struct {
Name string
Done bool
}
jobs := []job{{"lint", true}, {"test", false}, {"deploy", false}}
pending := catena.FromSlice(jobs).
FilterNot(func(j job) bool { return j.Done }).
Map(func(j job) string { return j.Name })
fmt.Println(pending.Collect())
jobs[1].Done = true
fmt.Println(pending.Collect())
}
Output: [test deploy] [deploy]
func Generate ¶
Generate yields seed, then next(seed), then next(next(seed)), forever. Infinite. Re-iterable iff next is pure.
Example ¶
package main
import (
"fmt"
"time"
"github.com/NerdMeNot/catena"
)
func main() {
// Exponential backoff. The seed is yielded first, then next applied
// repeatedly — forever, so Take decides how many retries there are.
backoff := catena.Generate(100*time.Millisecond, func(d time.Duration) time.Duration {
return d * 2
})
fmt.Println(backoff.Take(5).Collect())
}
Output: [100ms 200ms 400ms 800ms 1.6s]
func GenerateWhile ¶
GenerateWhile yields seed unconditionally, then successive next values until next reports false. Re-iterable iff next is pure.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Follows an API's "next page" links until there are none. The seed
// is yielded unconditionally; a value produced alongside ok=false —
// the empty link after the last page — is not.
nextLink := map[string]string{
"/orders?page=1": "/orders?page=2",
"/orders?page=2": "/orders?page=3",
}
pages := catena.GenerateWhile("/orders?page=1", func(url string) (string, bool) {
next, ok := nextLink[url]
return next, ok
})
fmt.Println(pages.Collect())
}
Output: [/orders?page=1 /orders?page=2 /orders?page=3]
func Intersect ¶
func Intersect[T comparable](a, b Seq[T]) Seq[T]
Intersect yields the distinct elements of a that occur in b, in a's encounter order. ⚠ Buffers all of b before a is consumed, plus a seen- set of a's distinct values.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// What both sides have — the users active last week who came back
// this week — in the order of the first operand.
lastWeek := catena.Of("ada", "bob", "eve", "dan")
thisWeek := catena.Of("eve", "zoe", "ada")
fmt.Println(catena.Intersect(lastWeek, thisWeek).Collect())
}
Output: [ada eve]
func NonZero ¶
func NonZero[T comparable](s Seq[T]) Seq[T]
NonZero yields the elements that are not the zero value of T.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Drops the zero value of T — the empty strings that splitting a
// path leaves behind here, but equally 0, nil pointers, or a zero
// struct.
segments := catena.FromSlice(strings.Split("/var/log//nginx/", "/"))
fmt.Println(catena.NonZero(segments).Collect())
}
Output: [var log nginx]
func Of ¶
Of returns a re-iterable Seq over the given values.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// For a handful of values known at the call site: the regions to
// probe, turned into the URLs to fetch.
regions := catena.Of("eu-west-1", "us-east-1")
regions.
Map(func(r string) string { return "https://" + r + ".api.example.com/health" }).
ForEach(func(url string) { fmt.Println(url) })
}
Output: https://eu-west-1.api.example.com/health https://us-east-1.api.example.com/health
func Once1 ¶
Once1 returns a re-iterable Seq of exactly one value. (Once, without the suffix, is the single-use guard method on Seq.)
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Lifts the one value you have into a sequence — here a header row
// to put ahead of the data. Once1, not Once: Once is the single-use
// guard method on Seq.
rows := catena.Of("ada,36", "bob,41")
fmt.Println(catena.Once1("name,age").Concat(rows).Collect())
}
Output: [name,age ada,36 bob,41]
func Range ¶
Range yields start, start+step, ... while the value is before stop (exclusive). Re-iterable. step == 0 panics at construction; a sign mismatch between step and the start→stop direction yields an empty sequence. Termination is overflow-guarded: a step past the type's edge stops rather than wrapping. Unsigned types cannot step downward.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Half-open, like a slice expression: the offsets of each page of 90
// results, then a countdown. A sign mismatch between step and
// direction yields nothing rather than panicking, so a computed step
// is safe.
total, pageSize := 90, 25
fmt.Println(catena.Range(0, total, pageSize).Collect())
fmt.Println(catena.Range(3, 0, -1).Collect())
fmt.Println(catena.Range(0, total, -pageSize).Collect())
}
Output: [0 25 50 75] [3 2 1] []
func Repeat ¶
Repeat yields v forever. Re-iterable. Infinite: pair with Take or a conditional terminal.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Infinite, so it must be bounded by something downstream — here
// Zip, which stops when the hosts run out, leaving every host with
// the same starting status.
hosts := catena.Of("web-1", "web-2", "db-1")
fmt.Println(catena.CollectMap(hosts.Zip(catena.Repeat("pending"))))
}
Output: map[db-1:pending web-1:pending web-2:pending]
func RepeatN ¶
RepeatN yields v exactly n times. Re-iterable. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"time"
"github.com/NerdMeNot/catena"
)
func main() {
// A fixed retry schedule: three attempts, the same wait before each.
fmt.Println(catena.RepeatN(500*time.Millisecond, 3).Collect())
}
Output: [500ms 500ms 500ms]
func Sorted ¶
Sorted yields the elements in ascending order, stably. NaN sorts first. ⚠ Buffers the entire input.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Ascending order for any ordered type. A map hands its keys back in a
// different order on every run; sorting them makes the output stable.
env := map[string]string{"PORT": "8080", "HOME": "/srv/app", "LOG_LEVEL": "info"}
fmt.Println(catena.Sorted(catena.FromMap(env).Keys()).Collect())
}
Output: [HOME LOG_LEVEL PORT]
func SortedDesc ¶
SortedDesc yields the elements in descending order, stably. ⚠ Buffers the entire input.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Largest first: the slowest responses, in milliseconds, at the top
// of the report.
latencies := catena.Of(87, 912, 45, 340)
fmt.Println(catena.SortedDesc(latencies).Collect())
}
Output: [912 340 87 45]
func Union ¶
func Union[T comparable](a, b Seq[T]) Seq[T]
Union yields the distinct elements of a, then the distinct elements of b not in a — set semantics, encounter order. ⚠ Retains one entry per distinct value — unbounded.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Set semantics: a permission granted twice, or by both the role and
// the team, appears once. Encounter order, left operand first.
fromRole := catena.Of("read", "write", "write")
fromTeam := catena.Of("deploy", "read")
fmt.Println(catena.Union(fromRole, fromTeam).Collect())
}
Output: [read write deploy]
func Windowed ¶
Windowed yields sliding windows of exactly size elements, advancing by step; trailing elements that do not fill a window are dropped. step > size is valid and samples with gaps. ⚠ Buffers size elements. Every window is a fresh slice. Panics if size or step is <= 0. A package function for the same instantiation-cycle reason as Chunked.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Overlapping windows: size 3 advancing by 1, which is a moving
// average — the spike is smoothed in rather than jumping. Only full
// windows are yielded, so five samples make three.
latencies := catena.Of(120, 130, 110, 400, 420)
for w := range catena.Windowed(latencies, 3, 1).Seq() {
fmt.Println(w, "avg", catena.Sum(catena.FromSlice(w))/len(w))
}
}
Output: [120 130 110] avg 120 [130 110 400] avg 213 [110 400 420] avg 310
func (Seq[T]) All ¶
All reports whether pred admits every element; stops at the first counterexample. Vacuously true on empty input.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Vacuously true on an empty sequence: a pipeline with no checks
// "passes", so guard for that separately if it matters.
type check struct {
Name string
Passed bool
}
passed := func(c check) bool { return c.Passed }
ci := catena.Of(check{"lint", true}, check{"unit", true}, check{"e2e", true})
fmt.Println(ci.All(passed))
fmt.Println(catena.Empty[check]().All(passed))
}
Output: true true
func (Seq[T]) Any ¶
Any reports whether pred admits any element; stops at the first match.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Stops at the first match, so it terminates even on an endless
// source. Here the two lines after the error are never read.
read := 0
log := catena.Of("INFO boot", "ERROR disk full", "INFO retrying", "INFO ok").
OnEach(func(string) { read++ })
failed := log.Any(func(line string) bool { return strings.HasPrefix(line, "ERROR") })
fmt.Println(failed, read)
}
Output: true 2
func (Seq[T]) Append ¶
Append yields s, then the given values.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Loose values on the end, without wrapping them in a sequence first:
// the flags a wrapper always adds after the user's own arguments.
userArgs := catena.Of("test", "./...")
fmt.Println(userArgs.Append("-race", "-count=1").Collect())
}
Output: [test ./... -race -count=1]
func (Seq[T]) Associate ¶
func (s Seq[T]) Associate[K comparable, V any](f func(T) (K, V)) map[K]V
Associate builds a map from f's key/value pairs; on duplicate keys the last pair wins. ⚠ Full drain; map iteration order is undefined.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Each element supplies both the key and the value.
env := catena.Of("HOME=/home/ada", "SHELL=/bin/zsh", "LANG=en_GB.UTF-8").
Associate(func(line string) (string, string) {
name, value, _ := strings.Cut(line, "=")
return name, value
})
fmt.Println(env["SHELL"], env["LANG"])
}
Output: /bin/zsh en_GB.UTF-8
func (Seq[T]) AverageOf ¶
AverageOf returns the mean of the selected values, accumulating in float64 (naive summation — precision for large integer inputs is not guaranteed); (0, false) on empty input. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Accumulates in float64, so integer latencies give a fractional
// mean — and reports false on empty rather than dividing by zero.
type request struct {
Path string
Ms int
}
ms := func(r request) int { return r.Ms }
requests := catena.Of(request{"/home", 120}, request{"/cart", 85}, request{"/search", 310})
mean, ok := requests.AverageOf(ms)
_, nonEmpty := catena.Empty[request]().AverageOf(ms)
fmt.Printf("%.2f %v %v\n", mean, ok, nonEmpty)
}
Output: 171.67 true false
func (Seq[T]) BottomNBy ¶
BottomNBy returns the n elements with the smallest keys, sorted ascending by key; equal keys retain encounter order. Memory is O(n) — the streaming alternative to SortedBy().Take(n). catena.BottomN(s, n) is the no-selector form. ⚠ Full drain. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The same bounded heap at the other end, sorted ascending: the two
// least-loaded servers to send new work to.
type server struct {
Host string
Load int
}
pool := catena.Of(server{"web-1", 71}, server{"web-2", 12}, server{"web-3", 94}, server{"web-4", 35})
fmt.Println(pool.BottomNBy(2, func(s server) int { return s.Load }))
}
Output: [{web-2 12} {web-4 35}]
func (Seq[T]) Collect ¶
func (s Seq[T]) Collect() []T
Collect drains the sequence into a slice; nil for empty. ToList is the same drain returning a List, which carries the eager operator set. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// The hand-off back to ordinary Go: a plain slice. Nothing matching
// gives nil, as slices.Collect does, so len and range need no guard.
addrs := catena.Of("web-1:443", "db-1:5432", "cache-1:6379")
onPort := func(port string) func(string) bool {
return func(addr string) bool { return strings.HasSuffix(addr, port) }
}
tls := addrs.Filter(onPort(":443")).Collect()
plain := addrs.Filter(onPort(":80")).Collect()
fmt.Println(tls, plain == nil)
}
Output: [web-1:443] true
func (Seq[T]) Concat ¶
Concat yields s, then each of the others in order.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// One sequence after another, any number of them: resolvers in the
// order they should be tried. No sequence is read until the one before
// it is exhausted.
primary := catena.Of("10.0.0.2", "10.0.0.3")
office := catena.Of("192.168.1.1")
public := catena.Of("1.1.1.1", "8.8.8.8")
fmt.Println(primary.Concat(office, public).Collect())
}
Output: [10.0.0.2 10.0.0.3 192.168.1.1 1.1.1.1 8.8.8.8]
func (Seq[T]) Count ¶
Count returns the number of elements. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Counts by consuming: nothing is collected along the way.
body := "the quick brown fox jumps over the lazy dog"
fmt.Println(catena.FromSlice(strings.Fields(body)).Count())
}
Output: 9
func (Seq[T]) CountWhere ¶
CountWhere returns the number of elements pred admits. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Fused filter and count: one stage rather than two.
statuses := catena.Of(200, 503, 200, 404, 500, 200)
fmt.Println(statuses.CountWhere(func(code int) bool { return code >= 500 }))
}
Output: 2
func (Seq[T]) DedupeBy ¶
func (s Seq[T]) DedupeBy[K comparable](sel func(T) K) Seq[T]
DedupeBy yields elements whose key differs from the previous element's key — consecutive duplicates only, O(1) memory. On key-sorted input it equals DistinctBy at a fraction of the cost.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Turns a stream of health checks into a stream of state changes.
// Only CONSECUTIVE repeats collapse, in O(1) memory, so the second
// "up" is kept — and it is safe on an unbounded source, where
// DistinctBy's seen-set would grow forever.
type check struct {
At, Status string
}
checks := catena.Of(
check{"09:00", "up"}, check{"09:01", "up"},
check{"09:02", "down"}, check{"09:03", "down"},
check{"09:04", "up"},
)
fmt.Println(checks.
DedupeBy(func(c check) string { return c.Status }).
Collect())
}
Output: [{09:00 up} {09:02 down} {09:04 up}]
func (Seq[T]) DistinctBy ¶
func (s Seq[T]) DistinctBy[K comparable](sel func(T) K) Seq[T]
DistinctBy yields elements whose key has not been seen before; the first occurrence wins. catena.Distinct(s) is the no-selector form, for a comparable T. On key-sorted input DedupeBy is the O(1)-memory equivalent. ⚠ Retains one key per distinct value — unbounded.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Page the on-call engineer once per host, not once per alert. The
// first alert seen for a host is the one that survives.
type alert struct {
Host, Problem string
}
alerts := catena.Of(
alert{"db-1", "disk 91% full"},
alert{"web-3", "5xx rate high"},
alert{"db-1", "replication lag"},
)
fmt.Println(alerts.
DistinctBy(func(a alert) string { return a.Host }).
Collect())
}
Output: [{db-1 disk 91% full} {web-3 5xx rate high}]
func (Seq[T]) DistinctWith ¶
DistinctWith yields elements no earlier element equals under eq. First occurrence wins. ⚠ Retains all distinct elements and compares in O(n²) — small inputs only.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// For an equality of your own — here, signups that differ only in
// letter case are the same person — or for keys that are not
// comparable. Retains every distinct element and compares against
// all of them, so this is for small inputs.
signups := catena.Of(
"Ada@example.com", "bob@example.com", "ada@example.com", "ADA@EXAMPLE.COM",
)
fmt.Println(signups.DistinctWith(strings.EqualFold).Collect())
}
Output: [Ada@example.com bob@example.com]
func (Seq[T]) Drain ¶
func (s Seq[T]) Drain()
Drain consumes the sequence for its side effects. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Runs a pipeline whose stages do the work, discarding the elements:
// here, warming a cache.
type user struct {
ID int
Name string
}
cache := map[int]string{}
catena.Of(user{7, "ada"}, user{9, "bob"}).
OnEach(func(u user) { cache[u.ID] = u.Name }).
Drain()
fmt.Println(len(cache), cache[9])
}
Output: 2 bob
func (Seq[T]) Drop ¶
Drop skips the first n elements. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Pagination: skip the pages already shown, then take one page.
results := catena.Of("ada", "bob", "cyd", "dee", "eve", "fay", "gus")
page, size := 1, 3
fmt.Println(results.Drop(page * size).Take(size).Collect())
}
Output: [dee eve fay]
func (Seq[T]) DropLast ¶
DropLast yields all but the final n elements, emitting with an n-element lag. Panics if n is negative. ⚠ Buffers n elements.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Strips a totals row off the end of an export without knowing how
// long the export is. It runs n elements behind the source, and
// buffers only those.
export := catena.Of("ada,36.00", "bob,41.50", "eve,12.25", "TOTAL,89.75")
fmt.Println(export.DropLast(1).Collect())
}
Output: [ada,36.00 bob,41.50 eve,12.25]
func (Seq[T]) DropWhile ¶
DropWhile skips elements until pred first returns false, then yields the rest.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Skips the comment banner at the top of a config file. Only the
// leading run is dropped; once the predicate fails, everything after
// is kept — including the later comment.
config := catena.Of(
"# managed by ops",
"# do not edit",
"port = 8080",
"# seconds",
"timeout = 30",
)
fmt.Println(config.
DropWhile(func(line string) bool { return strings.HasPrefix(line, "#") }).
JoinToString("; ", catena.Self[string]))
}
Output: port = 8080; # seconds; timeout = 30
func (Seq[T]) DuplicatesBy ¶ added in v1.5.0
func (s Seq[T]) DuplicatesBy[K comparable](sel func(T) K) Seq[T]
DuplicatesBy yields the elements that repeat a key: for each key seen more than once, the second element to have it, once. Order is the order in which keys were first repeated. catena.Duplicates(s) is the no-selector form, for a comparable T. ⚠ Retains one key per distinct value — unbounded.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The element reported is the one that repeats the key: here, the
// second signup with each address, which is the one to reject.
type signup struct {
Email string
Name string
}
signups := catena.Of(
signup{"ada@example.com", "Ada"},
signup{"bob@example.com", "Bob"},
signup{"ada@example.com", "Ada L."},
)
fmt.Println(signups.DuplicatesBy(func(s signup) string { return s.Email }).Collect())
}
Output: [{ada@example.com Ada L.}]
func (Seq[T]) ElementAt ¶
ElementAt returns the element at index i; (zero, false) for a negative or out-of-range index.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Zero-based. An index out of range, or negative, reports false
// rather than panicking — there is no counting from the end.
leaderboard := catena.Of("mira", "tom", "ada")
runnerUp, ok := leaderboard.ElementAt(1)
_, fromEnd := leaderboard.ElementAt(-1)
fmt.Println(runnerUp, ok, fromEnd)
}
Output: tom true false
func (Seq[T]) Filter ¶
Filter yields the elements for which pred returns true.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The unpaid orders worth a reminder email. Nothing is tested until
// Collect asks for the result.
type order struct {
ID string
Total float64
Paid bool
}
orders := catena.Of(
order{"A-1001", 42.50, true},
order{"A-1002", 310.00, false},
order{"A-1003", 18.99, false},
order{"A-1004", 129.00, false},
)
fmt.Println(orders.
Filter(func(o order) bool { return !o.Paid && o.Total >= 100 }).
Map(func(o order) string { return o.ID }).
Collect())
}
Output: [A-1002 A-1004]
func (Seq[T]) FilterErr ¶
FilterErr yields elements pred admits, as a Try; a failed pred call yields (zero, err).
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// A predicate that can fail produces a Try, so the caller chooses
// what a failure means rather than the pipeline deciding. Here a bad
// config entry is reported alongside the ports that did parse.
configured := catena.Of("8080", "443", "http", "22")
privileged := configured.FilterErr(func(s string) (bool, error) {
port, err := strconv.Atoi(s)
return port < 1024, err
})
kept, errs := privileged.CollectAll()
fmt.Println(kept, errs)
}
Output: [443 22] [strconv.Atoi: parsing "http": invalid syntax]
func (Seq[T]) FilterIndexed ¶
FilterIndexed yields the elements for which pred(index, element) returns true. The index counts source elements from 0.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The index is the line's position in the source, not among the
// survivors: line 0 is the header however many blank lines the
// predicate has dropped along the way.
export := catena.Of("host,port", "", "db-1,5432", "", "cache-1,6379")
fmt.Println(export.
FilterIndexed(func(i int, line string) bool { return i > 0 && line != "" }).
Collect())
}
Output: [db-1,5432 cache-1,6379]
func (Seq[T]) FilterMap ¶
FilterMap yields the mapped value for each element f reports true for — a fused Map + Filter.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Fused filter and map: keep the error lines and strip their prefix in
// one stage. Anything returning (value, bool) fits, and the test is
// not run a second time to get the value.
logs := catena.Of(
"INFO listening on :8080",
"ERROR db: connection refused",
"INFO GET /health 200",
"ERROR cache: timeout",
)
for msg := range logs.FilterMap(func(line string) (string, bool) {
return strings.CutPrefix(line, "ERROR ")
}).Seq() {
fmt.Println(msg)
}
}
Output: db: connection refused cache: timeout
func (Seq[T]) FilterNot ¶
FilterNot yields the elements for which pred returns false.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The negated form, for when the predicate reads better positively:
// "is a probe" is the natural question, and the traffic report wants
// everything that is not one.
isProbe := func(path string) bool {
return path == "/healthz" || path == "/metrics"
}
paths := catena.Of("/checkout", "/healthz", "/cart", "/metrics", "/login")
fmt.Println(paths.FilterNot(isProbe).Collect())
}
Output: [/checkout /cart /login]
func (Seq[T]) Find ¶
Find returns the first element pred admits.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The first request that broke the 500ms budget; nothing after it is
// examined.
type request struct {
Path string
Ms int
}
requests := catena.Of(request{"/home", 80}, request{"/search", 740}, request{"/export", 2100})
slow, ok := requests.Find(func(r request) bool { return r.Ms > 500 })
fmt.Println(slow.Path, ok)
}
Output: /search true
func (Seq[T]) FindIndex ¶
FindIndex returns the index of the first element pred admits; -1 if none.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Where the first match is, counted from zero; -1 when there is none.
log := catena.Of("INFO boot", "INFO listening on :8080", "ERROR disk full")
hasLevel := func(level string) func(string) bool {
return func(line string) bool { return strings.HasPrefix(line, level) }
}
fmt.Println(log.FindIndex(hasLevel("ERROR")))
fmt.Println(log.FindIndex(hasLevel("FATAL")))
}
Output: 2 -1
func (Seq[T]) FindLast ¶
FindLast returns the final element pred admits. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The most recent release that passed — the one to roll back to.
type deploy struct {
Version string
Healthy bool
}
history := catena.Of(deploy{"v41", true}, deploy{"v42", true}, deploy{"v43", false})
good, ok := history.FindLast(func(d deploy) bool { return d.Healthy })
fmt.Println(good.Version, ok)
}
Output: v42 true
func (Seq[T]) FindMap ¶
FindMap returns the first mapped value f reports true for — a fused Find + Map.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Fused find and map: the parsed value is returned, not the header
// line it came from, and the lines after it are never parsed.
headers := catena.Of("Host: example.com", "Content-Length: 348", "Accept: */*")
length, ok := headers.FindMap(func(h string) (int, bool) {
value, found := strings.CutPrefix(h, "Content-Length: ")
if !found {
return 0, false
}
n, err := strconv.Atoi(value)
return n, err == nil
})
fmt.Println(length, ok)
}
Output: 348 true
func (Seq[T]) First ¶
First returns the first element.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The bool tells an empty sequence apart from a first element that
// happens to be the zero value.
queue := catena.Of("deploy api", "rotate keys")
job, ok := queue.First()
_, idle := catena.Empty[string]().First()
fmt.Println(job, ok, idle)
}
Output: deploy api true false
func (Seq[T]) FirstOr ¶ added in v1.5.0
func (s Seq[T]) FirstOr(fallback T) T
FirstOr returns the first element, or fallback if the sequence is empty — First for use inside an expression.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// First, for use inside an expression: no second result to check.
ports := catena.Of(8080, 8081)
var none catena.Seq[int]
fmt.Println(ports.FirstOr(80), none.FirstOr(80))
}
Output: 8080 80
func (Seq[T]) FlatMap ¶
FlatMap yields all elements of f(v) for each element v, in order.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Each element expands into a sequence of its own and the results run
// together in order. The inner sequences stay lazy: no port list is
// built for a rule until the consumer reaches it.
type rule struct {
Service string
From, To int
}
rules := catena.Of(rule{"web", 8080, 8082}, rule{"metrics", 9100, 9101})
fmt.Println(rules.
FlatMap(func(r rule) catena.Seq[int] { return catena.Range(r.From, r.To+1, 1) }).
Collect())
}
Output: [8080 8081 8082 9100 9101]
func (Seq[T]) FlatMapSlice ¶
FlatMapSlice yields all elements of the slice f(v) for each element v.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The same, when each element already holds a slice: every file touched
// by a run of commits, with no wrapping of the slice in a sequence.
type commit struct {
Hash string
Files []string
}
commits := catena.Of(
commit{"a1f9", []string{"go.mod", "main.go"}},
commit{"c03d", []string{"README.md"}},
)
fmt.Println(commits.FlatMapSlice(func(c commit) []string { return c.Files }).Collect())
}
Output: [go.mod main.go README.md]
func (Seq[T]) Fold ¶
Fold reduces the sequence into an accumulator, left to right.
Reduce is the same operation using the first element as the initial accumulator; FoldBy folds per key in one streaming pass. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The seed is where the result starts: an opening balance of 100,
// then the day's deposits and withdrawals.
ledger := catena.Of(250, -75, -20)
balance := ledger.Fold(100, func(balance, amount int) int { return balance + amount })
fmt.Println(balance)
}
Output: 255
func (Seq[T]) FoldBy ¶
func (s Seq[T]) FoldBy[K comparable, A any]( key func(T) K, init func(K) A, f func(A, T) A, ) map[K]A
FoldBy folds each element into a per-key accumulator: streaming grouped aggregation with no intermediate per-key slices. init is called once per distinct key. Memory is bounded by the number of distinct keys, not elements. ⚠ Full drain; map iteration order is undefined.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Streaming aggregation per key. GroupBy would retain every sale;
// this retains one running total per region.
type sale struct {
Region string
Amount int
}
sales := catena.Of(
sale{"west", 100}, sale{"east", 40}, sale{"west", 20},
)
totals := sales.FoldBy(
func(s sale) string { return s.Region },
func(string) int { return 0 },
func(sum int, s sale) int { return sum + s.Amount },
)
fmt.Println(totals["west"], totals["east"])
}
Output: 120 40
func (Seq[T]) FoldErr ¶
FoldErr folds until f fails, returning the accumulator so far and the first error.
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// Stops at the first error and returns the accumulator so far: the
// total of the rows that did parse, and why the next one did not.
amounts := catena.Of("12", "30", "n/a", "7")
total, err := amounts.FoldErr(0, func(sum int, field string) (int, error) {
n, err := strconv.Atoi(field)
if err != nil {
return 0, err
}
return sum + n, nil
})
fmt.Println(total, err)
}
Output: 42 strconv.Atoi: parsing "n/a": invalid syntax
func (Seq[T]) FoldIndexed ¶
FoldIndexed is Fold with the element index. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// The fold knows where it is: gather the line number of every
// failing package.
results := catena.Of("ok pkg/api", "FAIL pkg/db", "ok pkg/web", "FAIL pkg/auth")
failedAt := results.FoldIndexed([]int(nil), func(i int, lines []int, line string) []int {
if strings.HasPrefix(line, "FAIL") {
return append(lines, i+1)
}
return lines
})
fmt.Println(failedAt)
}
Output: [2 4]
func (Seq[T]) FoldWhile ¶
FoldWhile folds until f reports false; the accumulator from the stopping call is included in the result.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Stops when the callback says so; the accumulator from the stopping
// call is included. The file that tips the batch past 10 MB is
// counted, and the one after it is never looked at.
fileMB := catena.Of(4, 3, 5, 2)
batchMB := fileMB.FoldWhile(0, func(total, size int) (int, bool) {
total += size
return total, total < 10
})
fmt.Println(batchMB)
}
Output: 12
func (Seq[T]) FoldedBy ¶ added in v1.5.0
func (s Seq[T]) FoldedBy[K comparable, A any]( key func(T) K, init func(K) A, f func(A, T) A, ) Seq2[K, A]
FoldedBy is FoldBy that stays in the chain: it folds each element into a per-key accumulator, then yields each key with its accumulator in the order the keys were first seen. init is called once per distinct key. Memory is bounded by the number of distinct keys, not elements. catena.GroupedBy is the same shape with every element kept. ⚠ Fully drains the source before the first emission — hangs on infinite input.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// FoldBy that stays in the chain: keys come out in the order they
// were first seen, so the result is deterministic and keeps flowing.
type hit struct {
Path string
Bytes int
}
hits := catena.Of(hit{"/checkout", 900}, hit{"/home", 2400}, hit{"/health", 2}, hit{"/checkout", 1100})
hits.
FoldedBy(
func(h hit) string { return h.Path },
func(string) int { return 0 },
func(sum int, h hit) int { return sum + h.Bytes },
).
Filter(func(_ string, bytes int) bool { return bytes >= 1000 }).
ForEach(func(path string, bytes int) { fmt.Println(path, bytes) })
}
Output: /checkout 2000 /home 2400
func (Seq[T]) ForEach ¶
func (s Seq[T]) ForEach(f func(T))
ForEach calls f on every element. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// The end of a pipeline whose point is the side effect.
results := catena.Of("ok pkg/api", "FAIL pkg/db", "ok pkg/web", "FAIL pkg/auth")
results.
Filter(func(line string) bool { return strings.HasPrefix(line, "FAIL") }).
ForEach(func(line string) { fmt.Println(line) })
}
Output: FAIL pkg/db FAIL pkg/auth
func (Seq[T]) ForEachErr ¶
ForEachErr calls f on every element, stopping at and returning the first non-nil error; nil if the sequence drains clean.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Stops at the first error the callback returns, and returns it:
// the order after the failure is never attempted.
type order struct{ ID, Card string }
orders := catena.Of(order{"A-1", "visa"}, order{"A-2", ""}, order{"A-3", "amex"})
err := orders.ForEachErr(func(o order) error {
if o.Card == "" {
return fmt.Errorf("order %s: no card on file", o.ID)
}
fmt.Println("charged", o.ID)
return nil
})
fmt.Println("err:", err)
}
Output: charged A-1 err: order A-2: no card on file
func (Seq[T]) ForEachIndexed ¶
ForEachIndexed calls f(index, element) on every element. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The position comes with the element, counted from zero.
steps := catena.Of("back up the database", "run migrations", "restart the API")
steps.ForEachIndexed(func(i int, step string) {
fmt.Printf("%d. %s\n", i+1, step)
})
}
Output: 1. back up the database 2. run migrations 3. restart the API
func (Seq[T]) GroupBy ¶
func (s Seq[T]) GroupBy[K comparable](sel func(T) K) map[K][]T
GroupBy collects elements into per-key buckets, each in encounter order. ⚠ Full drain; retains every element; map iteration order is undefined.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Retains every element. For an aggregate, FoldBy is bounded by keys.
type order struct {
Customer string
ID int
}
orders := catena.Of(order{"acme", 1001}, order{"globex", 1002}, order{"acme", 1003})
byCustomer := orders.GroupBy(func(o order) string { return o.Customer })
fmt.Println(len(byCustomer["acme"]), byCustomer["globex"])
}
Output: 2 [{globex 1002}]
func (Seq[T]) GroupJoinBy ¶ added in v1.5.0
func (s Seq[T]) GroupJoinBy[U any, K comparable, R any]( other Seq[U], leftKey func(T) K, rightKey func(U) K, combine func(T, []U) R, ) Seq[R]
GroupJoinBy pairs each element of s with all the elements of other sharing its key, yielding combine exactly once per left element — the one-to-many join that keeps the many together. matches is in right encounter order and nil when nothing matches. Its elements are shared by every left element with that key: retain it and append to it freely, but do not assign to its elements. ⚠ Buffers all of other before the first emission.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// One row per customer with all of their orders together — where
// JoinBy would repeat ada once per order and drop bob, who has none.
type order struct {
Customer int
Amount int
}
type customer struct {
ID int
Name string
}
customers := catena.Of(customer{1, "ada"}, customer{2, "bob"})
orders := catena.Of(order{1, 30}, order{1, 12}, order{9, 99})
fmt.Println(customers.GroupJoinBy(orders,
func(c customer) int { return c.ID },
func(o order) int { return o.Customer },
func(c customer, os []order) string { return fmt.Sprintf("%s:%d", c.Name, len(os)) },
).Collect())
}
Output: [ada:2 bob:0]
func (Seq[T]) IfEmpty ¶
IfEmpty yields s, or the given defaults if s yields nothing.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A fallback for the whole sequence, not per element: the default
// applies only when nothing at all was configured.
configured := catena.Of("eu-1.example.com", "eu-2.example.com")
unset := catena.Empty[string]()
fmt.Println(configured.IfEmpty("localhost").Collect())
fmt.Println(unset.IfEmpty("localhost").Collect())
}
Output: [eu-1.example.com eu-2.example.com] [localhost]
func (Seq[T]) IndexBy ¶
func (s Seq[T]) IndexBy[K comparable](sel func(T) K) map[K]T
IndexBy maps each key to its element; on duplicate keys the last element wins. ⚠ Full drain; map iteration order is undefined.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A lookup table; on a duplicate key the last element wins — which
// is what defaults followed by overrides want.
type setting struct{ Key, Value string }
config := catena.Of(
setting{"port", "8080"}, setting{"log", "info"}, // defaults
setting{"port", "9090"}, // override
).IndexBy(func(s setting) string { return s.Key })
fmt.Println(config["port"].Value, config["log"].Value)
}
Output: 9090 info
func (Seq[T]) Intersperse ¶
Intersperse yields sep between consecutive elements.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A separator between elements but not before the first or after the
// last — the fence-post problem, handled once.
menu := catena.Of("Open", "Save", "Quit")
for line := range menu.Intersperse("----").Seq() {
fmt.Println(line)
}
}
Output: Open ---- Save ---- Quit
func (Seq[T]) IsEmpty ¶
IsEmpty reports whether the sequence yields nothing. ⚠ It does so by consuming one element — on a single-pass source that element is lost.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Answers by consuming one element — on a single-pass source that
// element is gone.
statuses := catena.Of(200, 204, 301)
serverErrors := statuses.Filter(func(code int) bool { return code >= 500 })
fmt.Println(serverErrors.IsEmpty(), statuses.IsEmpty())
}
Output: true false
func (Seq[T]) IsSortedBy ¶ added in v1.5.0
IsSortedBy reports whether no element's key orders before its predecessor's; an empty or one-element sequence is sorted. sel is called once per element. It stops at the first pair out of order, so on infinite input it returns only if there is one.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Checks an ordering by one field without sorting anything: is the
// changelog oldest-first? ISO dates compare correctly as strings.
type release struct {
Tag string
Date string
}
changelog := catena.Of(
release{"v1.0.0", "2025-11-03"},
release{"v1.1.0", "2026-01-19"},
release{"v1.0.1", "2026-01-12"},
)
fmt.Println(changelog.IsSortedBy(func(r release) string { return r.Date }))
}
Output: false
func (Seq[T]) IsSortedWith ¶ added in v1.5.0
IsSortedWith reports whether no element orders before its predecessor under cmp; an empty or one-element sequence is sorted. It stops at the first pair out of order, so on infinite input it returns only if there is one. catena.IsSorted is the no-comparator form, for an ordered T.
Example ¶
package main
import (
"cmp"
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Any ordering a comparator can express: here, highest score first.
// A tie is still in order.
type player struct {
Name string
Score int
}
board := catena.Of(player{"ada", 90}, player{"bob", 85}, player{"eve", 85}, player{"dan", 70})
fmt.Println(board.IsSortedWith(func(a, b player) int { return cmp.Compare(b.Score, a.Score) }))
}
Output: true
func (Seq[T]) JoinBy ¶
func (s Seq[T]) JoinBy[U any, K comparable, R any]( other Seq[U], leftKey func(T) K, rightKey func(U) K, combine func(T, U) R, ) Seq[R]
JoinBy is a relational inner join: it pairs each element of s with every element of other sharing the same key and yields combine for each pair. It is unrelated to Join and JoinToString, which concatenate strings. Unmatched elements on either side are dropped; duplicate keys produce the cross product per key. Output order is left encounter order, then right encounter order within a key. ⚠ Buffers all of other before the first emission.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A relational inner join. Rows without a match are dropped on both
// sides — the order for unknown customer 9, and eve, who ordered
// nothing — and a key that matches twice produces a row each time.
type order struct {
Customer int
Amount int
}
type customer struct {
ID int
Name string
}
orders := catena.Of(order{1, 30}, order{2, 10}, order{9, 99}, order{1, 45})
customers := catena.Of(customer{1, "ada"}, customer{2, "bob"}, customer{3, "eve"})
fmt.Println(orders.JoinBy(customers,
func(o order) int { return o.Customer },
func(c customer) int { return c.ID },
func(o order, c customer) string { return fmt.Sprintf("%s:%d", c.Name, o.Amount) },
).Collect())
}
Output: [ada:30 bob:10 ada:45]
func (Seq[T]) JoinToString ¶
JoinToString concatenates the selected strings with sep between elements. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Map and join in one step: the To line of an email.
type user struct{ Name, Email string }
team := catena.Of(user{"Ada", "ada@example.com"}, user{"Bob", "bob@example.com"})
fmt.Println(team.JoinToString(", ", func(u user) string {
return fmt.Sprintf("%s <%s>", u.Name, u.Email)
}))
}
Output: Ada <ada@example.com>, Bob <bob@example.com>
func (Seq[T]) Last ¶
Last returns the final element. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The most recent of a series of health checks.
checks := catena.Of(200, 200, 503)
latest, ok := checks.Last()
fmt.Println(latest, ok)
}
Output: 503 true
func (Seq[T]) LastOr ¶ added in v1.5.0
func (s Seq[T]) LastOr(fallback T) T
LastOr returns the final element, or fallback if the sequence is empty. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
versions := catena.Of("v1", "v2", "v3")
fmt.Println(versions.LastOr("v0"), catena.Empty[string]().LastOr("v0"))
}
Output: v3 v0
func (Seq[T]) LeftJoinBy ¶ added in v1.5.0
func (s Seq[T]) LeftJoinBy[U any, K comparable, R any]( other Seq[U], leftKey func(T) K, rightKey func(U) K, combine func(T, U, bool) R, ) Seq[R]
LeftJoinBy is a relational left outer join: like JoinBy, but a left element with no match is kept, and combine is called for it once with the zero U and false. Matched elements call combine with true once per match, so duplicate keys produce the cross product per key. Output order is left encounter order, then right encounter order within a key. ⚠ Buffers all of other before the first emission.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A left outer join: the order with no customer on file is kept, and
// combine is told so, receiving a zero customer and false.
type order struct {
Customer int
Amount int
}
type customer struct {
ID int
Name string
}
orders := catena.Of(order{1, 30}, order{9, 99})
customers := catena.Of(customer{1, "ada"})
fmt.Println(orders.LeftJoinBy(customers,
func(o order) int { return o.Customer },
func(c customer) int { return c.ID },
func(o order, c customer, ok bool) string {
if !ok {
return fmt.Sprintf("guest:%d", o.Amount)
}
return fmt.Sprintf("%s:%d", c.Name, o.Amount)
},
).Collect())
}
Output: [ada:30 guest:99]
func (Seq[T]) Map ¶
Map yields f applied to each element.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The element type changes mid-chain — accounts in, addresses out —
// which is what generic methods made possible.
type account struct {
Name string
Email string
}
accounts := catena.Of(
account{"Ada", "ada@example.com"},
account{"Bob", "bob@example.com"},
)
fmt.Println(accounts.Map(func(a account) string { return a.Email }).Collect())
}
Output: [ada@example.com bob@example.com]
func (Seq[T]) MapErr ¶
MapErr yields f applied to each element as a Try; a failed call yields (zero, err).
Example ¶
package main
import (
"fmt"
"time"
"github.com/NerdMeNot/catena"
)
func main() {
// A mapping that can fail produces a Try, so one bad timeout in a
// config does not have to abort the rest. A failed call yields the zero
// value alongside its error, never a half-built one.
timeouts := catena.Of("30s", "soon", "5m")
parsed, errs := timeouts.MapErr(time.ParseDuration).CollectAll()
fmt.Println(parsed, len(errs))
}
Output: [30s 5m0s] 1
func (Seq[T]) MapIndexed ¶
MapIndexed yields f(index, element), counting from 0.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The position arrives with the element, so numbering needs no counter
// kept outside the chain. It counts from 0; a ranking wants i+1.
finishers := catena.Of("kiptum", "bekele", "farah")
fmt.Println(finishers.
MapIndexed(func(i int, name string) string { return fmt.Sprintf("%d. %s", i+1, name) }).
Collect())
}
Output: [1. kiptum 2. bekele 3. farah]
func (Seq[T]) MaxBy ¶
MaxBy returns the element with the largest key; the earliest maximal element wins ties. NaN keys order below everything (cmp.Compare). MaxOf returns the key itself; catena.Max(s) is the no-selector form. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Returns the ELEMENT with the largest key; ties go to the first.
type endpoint struct {
Path string
P99ms int
}
endpoints := catena.Of(endpoint{"/home", 120}, endpoint{"/search", 840}, endpoint{"/export", 840})
slowest, _ := endpoints.MaxBy(func(e endpoint) int { return e.P99ms })
fmt.Println(slowest.Path)
}
Output: /search
func (Seq[T]) MaxOf ¶
MaxOf returns the largest key — MaxBy returns the element that carries it. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Returns the KEY, where MaxBy returns the element — the -By/-Of
// distinction, which holds across the whole library.
type file struct {
Name string
Bytes int64
}
files := catena.Of(file{"app.log", 52_000}, file{"core.dump", 9_800_000}, file{"go.mod", 310})
largest, _ := files.MaxOf(func(f file) int64 { return f.Bytes })
fmt.Println(largest)
}
Output: 9800000
func (Seq[T]) MaxWith ¶
MaxWith returns the largest element under cmp; the earliest maximal element wins ties. ⚠ Full drain.
Example ¶
package main
import (
"cmp"
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A comparator, for orderings no single key expresses: major first,
// then minor.
type version struct{ Major, Minor int }
tags := catena.Of(version{1, 12}, version{2, 3}, version{2, 0})
latest, _ := tags.MaxWith(func(a, b version) int {
return cmp.Or(cmp.Compare(a.Major, b.Major), cmp.Compare(a.Minor, b.Minor))
})
fmt.Println(latest)
}
Output: {2 3}
func (Seq[T]) MinBy ¶
MinBy returns the element with the smallest key; the earliest minimal element wins ties. MinOf returns the key itself; catena.Min(s) is the no-selector form. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The whole quote comes back, not just its price.
type quote struct {
Carrier string
Cents int
}
quotes := catena.Of(quote{"dhl", 1250}, quote{"ups", 990}, quote{"fedex", 1400})
cheapest, _ := quotes.MinBy(func(q quote) int { return q.Cents })
fmt.Println(cheapest.Carrier)
}
Output: ups
func (Seq[T]) MinMaxOf ¶
MinMaxOf returns the smallest and largest keys in one pass. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Both ends in a single pass.
type reading struct {
Sensor string
TempC float64
}
readings := catena.Of(reading{"attic", 31.5}, reading{"cellar", 12.0}, reading{"hall", 20.5})
coldest, warmest, ok := readings.MinMaxOf(func(r reading) float64 { return r.TempC })
fmt.Println(coldest, warmest, ok)
}
Output: 12 31.5 true
func (Seq[T]) MinOf ¶
MinOf returns the smallest key — MinBy returns the element that carries it. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The lowest stock level, whichever product it belongs to.
type product struct {
SKU string
InStock int
}
shelf := catena.Of(product{"mug", 14}, product{"tee", 3}, product{"cap", 27})
lowest, _ := shelf.MinOf(func(p product) int { return p.InStock })
fmt.Println(lowest)
}
Output: 3
func (Seq[T]) MinWith ¶
MinWith returns the smallest element under cmp; the earliest minimal element wins ties. ⚠ Full drain.
Example ¶
package main
import (
"cmp"
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// What to work on next: the most urgent ticket, and among equally
// urgent ones the oldest.
type ticket struct {
ID string
Priority int // 1 is most urgent
AgeDays int
}
backlog := catena.Of(ticket{"T-7", 2, 30}, ticket{"T-8", 1, 2}, ticket{"T-9", 1, 6})
next, _ := backlog.MinWith(func(a, b ticket) int {
return cmp.Or(cmp.Compare(a.Priority, b.Priority), cmp.Compare(b.AgeDays, a.AgeDays))
})
fmt.Println(next.ID)
}
Output: T-9
func (Seq[T]) None ¶
None reports whether pred admits no element; stops at the first match.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// "Did anything go wrong?" asked the other way round; like Any, it
// stops at the first match.
statuses := catena.Of(200, 204, 301, 404)
fmt.Println(statuses.None(func(code int) bool { return code >= 500 }))
}
Output: true
func (Seq[T]) OnEach ¶
OnEach calls f on every element and passes it through unchanged.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Side effects without changing the stream — logging, metrics,
// progress. Elements pass through untouched, and nothing is logged
// until the terminal operation pulls them.
type upload struct {
Name string
Bytes int
}
uploads := catena.Of(upload{"report.pdf", 48_200}, upload{"logo.png", 9_100})
total := uploads.
OnEach(func(u upload) { fmt.Println("uploading", u.Name) }).
SumOf(func(u upload) int { return u.Bytes })
fmt.Println(total, "bytes")
}
Output: uploading report.pdf uploading logo.png 57300 bytes
func (Seq[T]) Once ¶
Once returns a sequence that panics if iterated more than once — a development guard for the single-pass contract, not a synchronization mechanism. This is the one operator whose state deliberately lives outside the iteration closure. (catena.Once1 is unrelated: it constructs a one-element sequence.)
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A development guard for sources that can only be read once — a
// request body, a database cursor: the second consumption panics
// instead of silently re-running the producer or reading nothing.
body := catena.Of("id,total", "1,40", "2,15").Once()
fmt.Println(body.Count(), "lines")
defer func() { fmt.Println("recovered:", recover()) }()
body.Collect()
}
Output: 3 lines recovered: catena: Once: sequence consumed more than once
func (Seq[T]) Partition ¶
Partition splits elements by pred, preserving encounter order on both sides; nil slices for empty sides. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Both sides in one pass, each in encounter order.
signups := catena.Of("ada@example.com", "bob", "eve@example.org", "n/a")
valid, rejected := signups.Partition(func(email string) bool {
return strings.Contains(email, "@")
})
fmt.Println(valid, rejected)
}
Output: [ada@example.com eve@example.org] [bob n/a]
func (Seq[T]) Prepend ¶
Prepend yields the given values, then s.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Loose values on the front: a header row ahead of the data, without
// building a second sequence just to hold it.
rows := catena.Of("report.pdf,48200", "logo.png,9100")
for line := range rows.Prepend("name,bytes").Seq() {
fmt.Println(line)
}
}
Output: name,bytes report.pdf,48200 logo.png,9100
func (Seq[T]) ProductOf ¶
ProductOf multiplies the selected values. Empty input yields 1, the multiplicative identity. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A request that needs every dependency is up only as often as all
// of them are. The empty product is 1, the multiplicative identity:
// nothing to depend on, nothing to fail.
type dependency struct {
Name string
Uptime float64
}
uptime := func(d dependency) float64 { return d.Uptime }
deps := catena.Of(dependency{"db", 0.999}, dependency{"cache", 0.995}, dependency{"auth", 0.99})
fmt.Printf("%.3f\n", deps.ProductOf(uptime))
fmt.Printf("%.3f\n", catena.Empty[dependency]().ProductOf(uptime))
}
Output: 0.984 1.000
func (Seq[T]) Pull ¶
Pull converts s to a pull-based iterator. THE CALLER MUST CALL stop, even if next has returned false, or resources held by s will leak.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Inverts control for hand-written loops, such as reading a header
// row before the records. THE CALLER MUST CALL stop, or a producer
// holding a resource never releases it.
rows := catena.Of("id,email", "1,ada@example.com", "2,bob@example.com")
next, stop := rows.Pull()
defer stop()
header, _ := next()
record, ok := next()
fmt.Println(header, "|", record, ok)
}
Output: id,email | 1,ada@example.com true
func (Seq[T]) Reduce ¶
Reduce folds the sequence using its first element as the initial accumulator; (zero, false) on empty input. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Uses the first element as the seed, so it suits values with no
// natural zero — and reports false on empty rather than inventing one.
type window struct{ From, To int }
merge := func(a, b window) window {
return window{min(a.From, b.From), max(a.To, b.To)}
}
outages := catena.Of(window{13, 14}, window{9, 11}, window{16, 17})
span, ok := outages.Reduce(merge)
_, nonEmpty := catena.Empty[window]().Reduce(merge)
fmt.Println(span, ok, nonEmpty)
}
Output: {9 17} true false
func (Seq[T]) Reversed ¶
Reversed yields the elements in reverse order. ⚠ Buffers the entire input — hangs on infinite input.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Last in, first out: migrations are rolled back in the opposite
// order to the one they were applied in.
applied := catena.Of("001_users", "002_orders", "003_add_index")
fmt.Println(applied.Reversed().Collect())
}
Output: [003_add_index 002_orders 001_users]
func (Seq[T]) Sample ¶ added in v1.5.0
Sample returns n elements chosen at random, every set of n being equally likely, in encounter order; all of them if the sequence has n or fewer. Memory is O(n) — reservoir sampling, the streaming alternative to collecting everything and shuffling. rng supplies the randomness, so a seeded one makes the choice repeatable; nil uses math/rand/v2's shared generator. ⚠ Full drain. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"math/rand/v2"
"github.com/NerdMeNot/catena"
)
func main() {
// Five of a million, each equally likely, holding five in memory.
// A seeded generator makes the choice repeatable; pass nil for the
// shared one.
rng := rand.New(rand.NewPCG(1, 2))
fmt.Println(len(catena.Range(0, 1_000_000, 1).Sample(5, rng)))
// A short sequence is returned whole, in order.
fmt.Println(catena.Of("a", "b").Sample(5, rng))
}
Output: 5 [a b]
func (Seq[T]) Scan ¶
Scan yields the running accumulator: f(init, e0), f(that, e1), ... The initial value itself is not yielded.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A running fold: the balance after every transaction rather than only
// the final one. The opening balance itself is not emitted, so the
// output is as long as the input.
opening := 500
transactions := catena.Of(120, -75, -300, 40)
fmt.Println(transactions.
Scan(opening, func(balance, amount int) int { return balance + amount }).
Collect())
}
Output: [620 545 245 285]
func (Seq[T]) Seq ¶
Seq converts to the stdlib iterator type. Free.
Example ¶
package main
import (
"fmt"
"slices"
"github.com/NerdMeNot/catena"
)
func main() {
// A free conversion to the standard iterator type, in both
// directions — Seq IS iter.Seq underneath — so anything in the
// standard library that takes an iterator takes a pipeline.
hosts := catena.Of("web-2", "db-1", "web-1")
fmt.Println(slices.Sorted(hosts.Seq()))
}
Output: [db-1 web-1 web-2]
func (Seq[T]) Single ¶
Single returns the element iff the sequence has exactly one; it stops consuming upon seeing a second.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// True only for exactly one element — the check for "there must be
// one primary". It stops as soon as a second arrives rather than
// counting the rest.
type node struct{ Name, Role string }
isPrimary := func(n node) bool { return n.Role == "primary" }
healthy := catena.Of(node{"db-1", "primary"}, node{"db-2", "replica"})
splitBrain := catena.Of(node{"db-1", "primary"}, node{"db-2", "primary"})
primary, ok := healthy.Filter(isPrimary).Single()
_, unambiguous := splitBrain.Filter(isPrimary).Single()
fmt.Println(primary.Name, ok, unambiguous)
}
Output: db-1 true false
func (Seq[T]) SortedBy ¶
SortedBy yields the elements sorted ascending by key, stably. sel is called exactly once per element (decorate-sort-undecorate). catena.Sorted(s) is the no-selector form, for an ordered T. ⚠ Buffers the entire input — hangs on infinite input.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Stable, and the selector runs exactly once per element rather than
// once per comparison — so an expensive key is affordable. Stability
// shows here: tickets of equal priority stay in the order they arrived.
type ticket struct {
ID string
Priority int
}
queue := catena.Of(ticket{"T-1", 2}, ticket{"T-2", 1}, ticket{"T-3", 2}, ticket{"T-4", 1})
fmt.Println(queue.SortedBy(func(t ticket) int { return t.Priority }).Collect())
}
Output: [{T-2 1} {T-4 1} {T-1 2} {T-3 2}]
func (Seq[T]) SortedByDesc ¶
SortedByDesc yields the elements sorted descending by key, stably. sel is called exactly once per element. ⚠ Buffers the entire input — hangs on infinite input.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Largest key first: what is filling the disk.
type file struct {
Name string
Bytes int
}
files := catena.Of(file{"app.log", 9_400}, file{"core.dump", 880_000}, file{"cache.db", 61_000})
fmt.Println(files.SortedByDesc(func(f file) int { return f.Bytes }).Collect())
}
Output: [{core.dump 880000} {cache.db 61000} {app.log 9400}]
func (Seq[T]) SortedWith ¶
SortedWith yields the elements sorted by cmp, stably. ⚠ Buffers the entire input — hangs on infinite input.
Example ¶
package main
import (
"cmp"
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// A comparator, for orderings a single key cannot express: by region,
// then busiest first within each region.
type host struct {
Region string
Name string
Load int
}
hosts := catena.Of(host{"us", "us-1", 40}, host{"eu", "eu-1", 15}, host{"us", "us-2", 85}, host{"eu", "eu-2", 60})
fmt.Println(hosts.
SortedWith(func(a, b host) int {
return cmp.Or(strings.Compare(a.Region, b.Region), cmp.Compare(b.Load, a.Load))
}).
Map(func(h host) string { return h.Name }).
Collect())
}
Output: [eu-2 eu-1 us-2 us-1]
func (Seq[T]) Step ¶
Step yields the first element and every nth element after it. Panics if n <= 0.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Downsampling: one CPU reading in every three is enough for the
// chart. The first element always survives, then every nth after it.
cpuPercent := catena.Of(12, 15, 11, 40, 42, 38, 90, 88)
fmt.Println(cpuPercent.Step(3).Collect())
}
Output: [12 40 90]
func (Seq[T]) SumOf ¶
SumOf sums the selected values; integer overflow wraps like +. Empty input sums to 0. catena.Sum(s) is the no-selector form. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The selector computes what to add, so a basket totals without an
// intermediate Map.
type line struct {
Qty, UnitCents int
}
basket := catena.Of(line{2, 1250}, line{1, 400}, line{3, 99})
fmt.Println(basket.SumOf(func(l line) int { return l.Qty * l.UnitCents }))
}
Output: 3197
func (Seq[T]) Take ¶
Take yields at most the first n elements, consuming exactly as many as it yields. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The next three invoice numbers from a counter that never ends.
// Take consumes exactly what it emits, so it bounds an infinite
// source.
invoices := catena.Generate(1041, func(n int) int { return n + 1 }).
Map(func(n int) string { return fmt.Sprintf("INV-%d", n) })
fmt.Println(invoices.Take(3).Collect())
}
Output: [INV-1041 INV-1042 INV-1043]
func (Seq[T]) TakeLast ¶
TakeLast yields the final n elements. ⚠ Buffers n elements and fully drains the source before emitting — hangs on infinite input. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// tail -n 2: the end of a log. It buffers only n lines, but has to
// read the source to its end to know which they are.
log := catena.Of(
"12:00:01 starting",
"12:00:02 listening on :8080",
"12:04:37 upstream timeout",
"12:04:38 shutting down",
)
log.TakeLast(2).ForEach(func(line string) { fmt.Println(line) })
}
Output: 12:04:37 upstream timeout 12:04:38 shutting down
func (Seq[T]) TakeWhile ¶
TakeWhile yields elements until pred first returns false.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The build steps that passed before the first failure. It stops at
// the first element that fails — unlike Filter, which would keep
// testing the rest and let "package" through.
type step struct {
Name string
Passed bool
}
steps := catena.Of(
step{"lint", true}, step{"build", true},
step{"test", false}, step{"package", true},
)
fmt.Println(steps.
TakeWhile(func(s step) bool { return s.Passed }).
Map(func(s step) string { return s.Name }).
Collect())
}
Output: [lint build]
func (Seq[T]) TallyBy ¶
func (s Seq[T]) TallyBy[K comparable](sel func(T) K) map[K]int
TallyBy counts elements per key. ⚠ Full drain; map iteration order is undefined.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A count per key, with no group ever built: responses by class.
statuses := catena.Of(200, 503, 201, 404, 500, 200)
byClass := statuses.TallyBy(func(code int) string { return fmt.Sprintf("%dxx", code/100) })
fmt.Println(byClass["2xx"], byClass["4xx"], byClass["5xx"])
}
Output: 3 1 2
func (Seq[T]) Then ¶ added in v1.5.0
Then returns f(s): it lets a function that takes a sequence sit inside a chain instead of wrapping it. That is every package function of one argument — s.Then(catena.Distinct).Then(catena.Sorted), or s.Then(catena.Sum) to finish — and every stage built by the parallel package. A function needing more arguments goes in a closure.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Package functions sit inside the chain instead of wrapping it, so
// the steps read in the order they happen: dedupe the hosts, sort
// them, roll out two at a time. A function taking more than the
// sequence goes in a closure.
hosts := catena.Of("web-2", "web-1", "web-2", "web-3", "web-1")
fmt.Println(hosts.
Then(catena.Distinct).
Then(catena.Sorted).
Then(func(s catena.Seq[string]) catena.Seq[[]string] { return catena.Chunked(s, 2) }).
Collect())
}
Output: [[web-1 web-2] [web-3]]
func (Seq[T]) ToChan ¶
ToChan starts a goroutine that sends every element on the returned unbuffered channel. The channel is closed when the sequence ends or ctx is done — the consumer must drain or cancel, or the goroutine leaks.
Example ¶
package main
import (
"context"
"fmt"
"sync"
"sync/atomic"
"github.com/NerdMeNot/catena"
)
func main() {
// The fan-out mechanism: a Seq is not safe to consume from two
// goroutines, a channel is. Cancelling ctx closes the channel.
uploads := catena.Of("a.png", "b.png", "c.png", "d.png")
jobs := uploads.ToChan(context.Background())
var resized atomic.Int32
var workers sync.WaitGroup
for range 2 {
workers.Go(func() {
for range jobs {
resized.Add(1)
}
})
}
workers.Wait()
fmt.Println(resized.Load())
}
Output: 4
func (Seq[T]) ToList ¶
ToList drains the sequence into a List; nil for empty. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The eager twin: a List has the same operations, evaluated at once,
// so it can be measured, indexed and reused without re-running the
// pipeline.
releases := catena.Of("v1.4.0", "v1.4.1", "v1.5.0").ToList()
fmt.Println(releases.Len(), releases.At(releases.Len()-1))
}
Output: 3 v1.5.0
func (Seq[T]) TopNBy ¶
TopNBy returns the n elements with the largest keys, sorted descending by key; equal keys retain encounter order and the earliest elements win at the cut. Memory is O(n) — the streaming alternative to SortedByDesc().Take(n). catena.TopN(s, n) is the no-selector form, and BottomNBy is the same operation at the other end. ⚠ Full drain. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A bounded heap of n, not a sort: memory is O(n) rather than O(all),
// which is the difference between a working pipeline and an OOM on a
// large scan. Output is sorted descending, ties in encounter order.
type customer struct {
Name string
Spend int
}
customers := catena.Of(
customer{"acme", 5200}, customer{"initech", 800}, customer{"globex", 9100},
customer{"hooli", 5200}, customer{"umbrella", 300},
)
fmt.Println(customers.TopNBy(3, func(c customer) int { return c.Spend }))
}
Output: [{globex 9100} {acme 5200} {hooli 5200}]
func (Seq[T]) UntilDone ¶
UntilDone passes elements through until ctx is done, then yields (zero, ctx.Err()) and stops.
Example ¶
package main
import (
"context"
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Cancellation enters at the edge rather than threading a context
// through every stage: an endless polling loop ends when the context
// does, and the context's error arrives as the pipeline's error.
ctx, cancel := context.WithCancel(context.Background())
polls := catena.Generate(1, func(attempt int) int { return attempt + 1 })
done, err := polls.UntilDone(ctx).
OnEach(func(attempt int) {
if attempt == 3 { // the deadline passes, or the user hits Ctrl-C
cancel()
}
}).
Collect()
fmt.Println(done, err)
}
Output: [1 2 3] context canceled
func (Seq[T]) WithIndex ¶
WithIndex pairs each element with its index, counting from 0.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// The index becomes half of a pair, so it survives later stages: the
// positions reported are those in the original file, counting from 0,
// not those in the filtered result.
lines := catena.Of("package main", "// TODO: add flags", "func main() {", "// TODO: exit code", "}")
fmt.Println(lines.WithIndex().
Filter(func(_ int, line string) bool { return strings.Contains(line, "TODO") }).
Keys().
Collect())
}
Output: [1 3]
func (Seq[T]) Zip ¶
Zip pairs elements of s with elements of other, stopping at the shorter side. The receiver drives; other is consumed through iter.Pull (its cleanup always runs). other is pulled once per emitted pair; the receiver is consumed one element past the pair count when other is shorter.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Pairs elements positionally and stops at the shorter side: a CSV row
// that is missing its last field yields only the columns it has.
header := catena.Of("host", "port", "region")
row := catena.Of("db-1", "5432")
fmt.Println(header.Zip(row).
MapTo(func(column, value string) string { return column + "=" + value }).
Collect())
}
Output: [host=db-1 port=5432]
func (Seq[T]) ZipWithNext ¶
ZipWithNext yields each adjacent pair (element, next element). Empty and single-element input yield nothing.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Each element paired with its successor — deltas, gaps, transitions.
// Here, the seconds between heartbeats; the long gap is the outage.
// Four heartbeats make three gaps.
heartbeats := catena.Of(0, 30, 61, 245)
fmt.Println(heartbeats.ZipWithNext().
MapTo(func(prev, next int) int { return next - prev }).
Collect())
}
Output: [30 31 184]
type Seq2 ¶
Seq2 is a lazy pair sequence: iter.Seq2 with methods. It is a bridge back to Seq (via Keys, Values, MapTo), deliberately not a full peer surface.
Deliberately absent, so nobody goes looking:
- ToMap: needs K comparable on the receiver. Use catena.CollectMap.
- Collect: catena.Unzip is the one way to two slices.
- MapKeys: Swap().MapValues(f).Swap() for the rare need; Map otherwise.
func Empty2 ¶
Empty2 returns the empty Seq2. Re-iterable.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The pair-sequence counterpart: a lookup with no matches still
// collects to a usable, empty map.
labels := catena.Empty2[string, string]()
fmt.Println(labels.Count(), catena.CollectMap(labels))
}
Output: 0 map[]
func From2 ¶
From2 adapts any push-function pair sequence. Re-iterable iff the underlying source is.
Example ¶
package main
import (
"fmt"
"slices"
"github.com/NerdMeNot/catena"
)
func main() {
// Any func(yield func(K, V) bool) adapts — here the standard
// library's index/value iterator, for progress labels.
steps := []string{"checkout", "build", "test"}
catena.From2(slices.All(steps)).
MapTo(func(i int, step string) string {
return fmt.Sprintf("[%d/%d] %s", i+1, len(steps), step)
}).
ForEach(func(label string) { fmt.Println(label) })
}
Output: [1/3] checkout [2/3] build [3/3] test
func FromMap ¶
func FromMap[K comparable, V any](m map[K]V) Seq2[K, V]
FromMap returns a re-iterable Seq2 over m, in undefined (map) order.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Which products are sold out? A map iterates in no particular
// order, so sort before anything order-sensitive such as printing.
stock := map[string]int{"keyboard": 12, "monitor": 0, "webcam": 0, "mouse": 40}
soldOut := catena.FromMap(stock).
Filter(func(_ string, qty int) bool { return qty == 0 }).
Keys()
fmt.Println(catena.Sorted(soldOut).Collect())
}
Output: [monitor webcam]
func GroupedBy ¶ added in v1.5.0
func GroupedBy[T any, K comparable](s Seq[T], sel func(T) K) Seq2[K, []T]
GroupedBy is GroupBy that stays in the chain: it yields each key with its bucket, keys in the order they were first seen and each bucket in encounter order. Every bucket is a fresh slice. Seq.FoldedBy is the form that keeps an accumulator per key instead of every element. ⚠ Fully drains the source before the first emission and retains every element — hangs on infinite input. A package function for the same instantiation- cycle reason as Chunked: Seq2[K, []T] leads back to Seq[[]T].
Example ¶
package main
import (
"fmt"
"path/filepath"
"github.com/NerdMeNot/catena"
)
func main() {
// GroupBy in first-seen key order, as a sequence of pairs rather
// than a map — so it can be ranged over and printed as it stands.
files := catena.Of("main.go", "README.md", "server.go", "deploy.yaml")
for ext, names := range catena.GroupedBy(files, filepath.Ext) {
fmt.Println(ext, names)
}
}
Output: .go [main.go server.go] .md [README.md] .yaml [deploy.yaml]
func (Seq2[K, V]) All ¶
All reports whether pred admits every pair; stops at the first counterexample. Vacuously true on empty input.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Is every required setting filled in? Stops at the first one that is
// not.
settings := map[string]string{"host": "localhost", "port": "8080", "token": ""}
complete := catena.FromMap(settings).All(func(_, value string) bool { return value != "" })
fmt.Println(complete)
}
Output: false
func (Seq2[K, V]) Any ¶
Any reports whether pred admits any pair; stops at the first match.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Is anything out of stock? Stops at the first pair that says yes.
stock := map[string]int{"mug": 4, "tee": 9, "cap": 0}
fmt.Println(catena.FromMap(stock).Any(func(_ string, units int) bool { return units == 0 }))
}
Output: true
func (Seq2[K, V]) Count ¶
Count returns the number of pairs. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// How many SKUs are running low.
stock := map[string]int{"mug": 4, "tee": 9, "cap": 0}
low := catena.FromMap(stock).Filter(func(_ string, units int) bool { return units < 5 })
fmt.Println(low.Count())
}
Output: 2
func (Seq2[K, V]) Drop ¶
Drop skips the first n pairs. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Skip a CSV header after numbering the lines, so the data rows keep
// the line numbers a user would see in their editor.
rows := catena.Of("sku,qty", "mug,4", "tee,9").WithIndex().Drop(1)
rows.ForEach(func(i int, row string) { fmt.Printf("line %d: %s\n", i+1, row) })
}
Output: line 2: mug,4 line 3: tee,9
func (Seq2[K, V]) Filter ¶
Filter yields the pairs pred admits.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The predicate sees both sides of the pair: pick hosts by their
// measured latency.
latencies := catena.Of("api-1", "api-2", "db-1").Zip(catena.Of(42, 310, 275))
slow := latencies.Filter(func(host string, ms int) bool { return ms > 200 })
fmt.Println(slow.Keys().Collect())
}
Output: [api-2 db-1]
func (Seq2[K, V]) FilterNot ¶
FilterNot yields the pairs pred rejects.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// Drop the comment lines of a config file while every surviving line
// keeps its original number, ready for an error message.
lines := catena.Of("# cache", "ttl=60", "# auth", "realm=prod").WithIndex().
FilterNot(func(_ int, line string) bool { return strings.HasPrefix(line, "#") })
lines.ForEach(func(i int, line string) { fmt.Printf("line %d: %s\n", i+1, line) })
}
Output: line 2: ttl=60 line 4: realm=prod
func (Seq2[K, V]) First ¶
First returns the first pair.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The first item that cannot be fulfilled, with the comma-ok form for
// when there is none. Nothing after it is examined.
stock := catena.Of("mug", "cap", "tee").Zip(catena.Of(4, 0, 0))
sku, units, ok := stock.Filter(func(_ string, units int) bool { return units == 0 }).First()
fmt.Println(sku, units, ok)
}
Output: cap 0 true
func (Seq2[K, V]) Fold ¶
Fold reduces the pairs into an accumulator, left to right. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A cart total: each pair is a quantity and a unit price in cents.
cart := catena.Of(2, 1, 3).Zip(catena.Of(1200, 2500, 400))
total := cart.Fold(0, func(sum, qty, cents int) int { return sum + qty*cents })
fmt.Println(total)
}
Output: 6100
func (Seq2[K, V]) ForEach ¶
func (s Seq2[K, V]) ForEach(f func(K, V))
ForEach calls f on every pair. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Both sides arrive as separate arguments — no pair type to unpack.
env := catena.Of("HOST", "PORT").Zip(catena.Of("localhost", "8080"))
env.ForEach(func(name, value string) {
fmt.Printf("%s=%s\n", name, value)
})
}
Output: HOST=localhost PORT=8080
func (Seq2[K, V]) Keys ¶
Keys yields the first element of each pair. Calling Keys and Values on the same single-pass Seq2 is a double consume — use Unzip.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The settings a config map defines, in a stable order. Keys and
// Values on the SAME single-pass Seq2 is a double consume; Unzip does
// both in one pass.
config := map[string]string{"port": "8080", "host": "localhost", "debug": "true"}
fmt.Println(catena.FromMap(config).Keys().Then(catena.Sorted).Collect())
}
Output: [debug host port]
func (Seq2[K, V]) Map ¶
Map yields f applied to each pair.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Both sides can change, types included: a ranked list becomes a
// name-to-place lookup.
finishers := catena.Of("ada", "grace", "linus").WithIndex()
places := finishers.Map(func(i int, name string) (string, int) { return name, i + 1 })
fmt.Println(catena.CollectMap(places))
}
Output: map[ada:1 grace:2 linus:3]
func (Seq2[K, V]) MapTo ¶
MapTo collapses each pair into one value — the intended exit back to Seq and its full API.
Example ¶
package main
import (
"fmt"
"net"
"github.com/NerdMeNot/catena"
)
func main() {
// The intended exit: collapse each pair into one value and continue
// in Seq, where the full API lives.
hosts := catena.Of("api.internal", "db.internal")
addrs := hosts.Zip(catena.Of("443", "5432")).MapTo(net.JoinHostPort)
fmt.Println(addrs.Collect())
}
Output: [api.internal:443 db.internal:5432]
func (Seq2[K, V]) MapValues ¶
MapValues yields each pair with its value replaced by f(k, v). f receives the key too (Kotlin-consistent).
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Keys stay put and values are rewritten. The callback receives the
// key as well, so the new value can depend on it — here, which items
// are on sale.
onSale := map[string]bool{"mug": true}
prices := catena.Of("mug", "tee").Zip(catena.Of(1200, 2500)).
MapValues(func(sku string, cents int) int {
if onSale[sku] {
return cents / 2
}
return cents
})
fmt.Println(catena.CollectMap(prices))
}
Output: map[mug:600 tee:2500]
func (Seq2[K, V]) Pull ¶
Pull converts s to a pull-based iterator. THE CALLER MUST CALL stop, even if next has returned false, or resources held by s will leak.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// When the consumer has to drive: take one pair now and the rest
// later. Ownership transfers to you: always call stop.
fields := catena.Of("id", "email").Zip(catena.Of("42", "ada@example.com"))
next, stop := fields.Pull()
defer stop()
column, value, ok := next()
fmt.Println(column, value, ok)
}
Output: id 42 true
func (Seq2[K, V]) Seq2 ¶
Seq2 converts to the stdlib iterator type. Free.
Example ¶
package main
import (
"fmt"
"maps"
"github.com/NerdMeNot/catena"
)
func main() {
// The standard iterator type, for the stdlib functions that take one —
// here maps.Insert, layering overrides onto defaults.
config := map[string]string{"host": "localhost", "port": "8080"}
overrides := catena.Of("port", "debug").Zip(catena.Of("9090", "true"))
maps.Insert(config, overrides.Seq2())
fmt.Println(config)
}
Output: map[debug:true host:localhost port:9090]
func (Seq2[K, V]) Swap ¶
Swap yields each pair with its sides exchanged.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Invert a lookup table: from code-to-name to name-to-code.
names := map[int]string{200: "OK", 404: "Not Found"}
codes := catena.CollectMap(catena.FromMap(names).Swap())
fmt.Println(codes["Not Found"], codes)
}
Output: 404 map[Not Found:404 OK:200]
func (Seq2[K, V]) Take ¶
Take yields at most the first n pairs. Panics if n is negative.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// The podium: the first three finishers, each still paired with its
// position.
finishers := catena.Of("ada", "grace", "linus", "ken", "dennis").WithIndex()
finishers.Take(3).ForEach(func(i int, name string) {
fmt.Printf("%d. %s\n", i+1, name)
})
}
Output: 1. ada 2. grace 3. linus
func (Seq2[K, V]) Values ¶
Values yields the second element of each pair.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// Leave the keys behind once only the values matter: total units in
// stock, whatever the SKU.
stock := map[string]int{"mug": 4, "tee": 9, "cap": 0}
fmt.Println(catena.FromMap(stock).Values().Then(catena.Sum))
}
Output: 13
type Try ¶
Try is a lazy sequence of fallible elements: iter.Seq2[T, error] with methods. When err != nil the value must not be read.
Try carries a small operator set on purpose — it is not a second Seq. The intended shape is to stay in Try only while errors are still in play, then commit to a policy (Ignore, Must, Collect, CollectAll) and continue on Seq with the full API. Wanting Sorted or GroupBy on a Try means the errors have been carried one stage too far.
Its operators follow five uniform rules, referred to below as R1–R5:
R1 Intermediates never inspect errored elements: predicates and map
functions are not called on them; the element flows through.
R2 The positional intermediates (Take, Drop) count elements, errored
or not, so Take(n) consumes at most n of the source.
Ignore().Take(n) is the "n successes" spelling. Count is a
terminal and follows R5 instead.
R3 An errored element passes through TakeWhile without terminating
the sequence; only a successful element failing pred ends it.
R4 Operators that generate an error yield (zero, err).
R5 Single-error terminals (Collect, Fold, ForEach, Err, Count) stop
consuming at the first error.
func EmptyTry ¶
EmptyTry returns the empty Try. Re-iterable.
Example ¶
package main
import (
"fmt"
"github.com/NerdMeNot/catena"
)
func main() {
// A fallible source with nothing in it, such as a stubbed query in a
// test: no rows, and no error either.
rows, err := catena.EmptyTry[string]().Collect()
fmt.Println(len(rows), err)
}
Output: 0 <nil>
func FromErrs ¶
FromErrs adapts any push-function fallible sequence. Re-iterable iff the underlying source is.
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// Wraps a source that can fail part-way — a parser, a database
// cursor, a network stream — so the error travels with the values
// instead of being checked at every step.
fields := []string{"21.5", "22.1", "n/a", "23.0"}
readings := catena.FromErrs(func(yield func(float64, error) bool) {
for _, f := range fields {
if !yield(strconv.ParseFloat(f, 64)) {
return
}
}
})
vals, err := readings.Collect()
fmt.Println(vals, err)
}
Output: [21.5 22.1] strconv.ParseFloat: parsing "n/a": invalid syntax
func (Try[T]) Collect ¶
Collect gathers successful elements until the first error, returning the partial slice and that error (R5); (all elements, nil) on a clean drain. Nil slice for empty.
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// The all-or-nothing policy: stop at the first bad value. You get the
// error plus what was gathered before it — enough to say how far it
// got, and "9090" is never parsed.
ports, err := catena.Of("8080", "https", "9090").MapErr(strconv.Atoi).Collect()
fmt.Println(ports)
fmt.Println(err)
}
Output: [8080] strconv.Atoi: parsing "https": invalid syntax
func (Try[T]) CollectAll ¶
CollectAll drains everything, gathering all successes and all errors. Positional correspondence between the two slices is lost. Nil slices when empty. ⚠ Full drain.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// The import-report policy: everything that worked and everything that
// did not, in one pass. The two slices do not correspond positionally.
orders, errs := importedOrders().CollectAll()
fmt.Printf("imported %d, rejected %d\n", len(orders), len(errs))
for _, err := range errs {
fmt.Println("rejected:", err)
}
}
Output: imported 3, rejected 1 rejected: order A-101: bad amount "pending"
func (Try[T]) Count ¶
Count counts successful elements up to the first error, which is returned alongside the count so far (R5).
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// Count is a terminal, so it stops at the first error: this is how
// many rows were good before the import broke, not how many are good
// in total. That would be Ignore().Count().
n, err := importedOrders().Count()
fmt.Println(n, err)
fmt.Println(importedOrders().Ignore().Count())
}
Output: 1 order A-101: bad amount "pending" 3
func (Try[T]) Drop ¶
Drop skips the first n elements, errored or not (R2). Panics if n is negative.
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// Skipping a header row. The header fails to parse like any other bad
// row, and Drop counts it all the same: it skips elements, errored or
// not.
column := catena.Of("qty", "12", "n/a", "7").MapErr(strconv.Atoi)
quantities, errs := column.Drop(1).CollectAll()
fmt.Println(quantities, errs)
}
Output: [12 7] [strconv.Atoi: parsing "n/a": invalid syntax]
func (Try[T]) Err ¶
Err consumes until the first error and returns it; nil on a clean drain (R5).
Example ¶
package main
import (
"fmt"
"net/netip"
"github.com/NerdMeNot/catena"
)
func main() {
// A pre-flight check: is every entry in the allowlist a valid address?
// Only the first error matters, and the parsed values are not needed.
allowlist := catena.Of("10.0.0.1", "10.0.0.256", "10.0.0.3").MapErr(netip.ParseAddr)
if err := allowlist.Err(); err != nil {
fmt.Println("allowlist rejected:", err)
}
}
Output: allowlist rejected: ParseAddr("10.0.0.256"): IPv4 field has value >255
func (Try[T]) Errs ¶
Errs yields the errors, dropping successful elements — the dual of Ignore. Ignore and Errs on the same single-pass Try is a double consume; use CollectAll.
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// Only what went wrong: a validation pass that lists every bad value
// and has no use for the good ones. It is the dual of Ignore; wanting
// both from one single-pass Try is a double consume, so use CollectAll.
flags := catena.Of("true", "yes", "false", "enabled").MapErr(strconv.ParseBool)
flags.Errs().ForEach(func(err error) { fmt.Println(err) })
}
Output: strconv.ParseBool: parsing "yes": invalid syntax strconv.ParseBool: parsing "enabled": invalid syntax
func (Try[T]) Filter ¶
Filter yields the successful elements pred admits; errored elements pass through unexamined.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// Keep the orders worth reviewing by hand. The predicate is not called
// on the row that failed, and that row is not filtered out — dropping
// it would silently discard the failure.
large := importedOrders().Filter(func(o importedOrder) bool { return o.Cents >= 1000 })
orders, errs := large.CollectAll()
fmt.Println(orders)
fmt.Println(errs)
}
Output: [{A-100 2500} {A-102 1200}] [order A-101: bad amount "pending"]
func (Try[T]) FilterErr ¶
FilterErr yields the successful elements pred admits; a failed pred call yields (zero, err); errored elements pass through unexamined.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// A predicate that has to look something up can fail too. A failed
// lookup takes the element's place as an error instead of being read
// as "no" and quietly dropping the order.
paid := map[string]bool{"A-100": true, "A-102": false}
settled := importedOrders().FilterErr(func(o importedOrder) (bool, error) {
isPaid, found := paid[o.ID]
if !found {
return false, fmt.Errorf("order %s: no payment record", o.ID)
}
return isPaid, nil
})
orders, errs := settled.CollectAll()
fmt.Println(orders)
fmt.Println(errs)
}
Output: [{A-100 2500}] [order A-101: bad amount "pending" order A-103: no payment record]
func (Try[T]) FlatMap ¶
FlatMap yields every element of f(v) for each successful element v, in order; an errored input element passes through un-mapped.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
func main() {
// One fallible step fans out into more fallible steps: every upload
// that opened is parsed value by value. The upload that failed to open
// passes through as its one error, in order with the inner ones.
uploads := map[string]string{"mon.csv": "12,7", "wed.csv": "3,n/a"}
open := func(name string) (string, error) {
if body, ok := uploads[name]; ok {
return body, nil
}
return "", fmt.Errorf("open %s: no such file", name)
}
quantities := catena.Of("mon.csv", "tue.csv", "wed.csv").MapErr(open).
FlatMap(func(body string) catena.Try[int] {
return catena.FromSlice(strings.Split(body, ",")).MapErr(strconv.Atoi)
})
vals, errs := quantities.CollectAll()
fmt.Println(vals, len(errs))
}
Output: [12 7 3] 2
func (Try[T]) Fold ¶
Fold reduces successful elements until the first error, returning the accumulator so far and that error (R5).
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// A revenue total over rows that may not parse. It stops at the first
// error and returns the accumulator so far alongside it, so a partial
// total can never be mistaken for the whole.
cents, err := importedOrders().Fold(0, func(sum int, o importedOrder) int {
return sum + o.Cents
})
fmt.Println(cents, err)
}
Output: 2500 order A-101: bad amount "pending"
func (Try[T]) ForEach ¶
ForEach calls f on each successful element, stopping at and returning the first of an element error or a non-nil f return (R5).
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// Do something fallible with each element — here, save it. Stops at
// the first failure from either side: a bad element, or the callback
// returning an error.
err := importedOrders().ForEach(func(o importedOrder) error {
fmt.Println("saved", o.ID)
return nil
})
fmt.Println("stopped:", err)
}
Output: saved A-100 stopped: order A-101: bad amount "pending"
func (Try[T]) Ignore ¶
Ignore yields the successful elements, dropping errored ones.
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// The log-scan policy: a malformed line is not worth stopping for.
// Drop the failures and carry on as a plain Seq, with its full API.
statuses := catena.Of("200", "503", "-", "404", "500").MapErr(strconv.Atoi)
serverErrors := statuses.Ignore().
Filter(func(code int) bool { return code >= 500 }).
Collect()
fmt.Println(serverErrors)
}
Output: [503 500]
func (Try[T]) Map ¶
Map yields f applied to each successful element; errored elements pass through.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// The callback only runs on rows that parsed. The bad row passes
// through as its error, so a failure is never formatted into something
// that looks like a valid receipt line.
receipts := importedOrders().Map(func(o importedOrder) string {
return fmt.Sprintf("%s $%.2f", o.ID, float64(o.Cents)/100)
})
lines, errs := receipts.CollectAll()
fmt.Println(lines)
fmt.Println(errs)
}
Output: [A-100 $25.00 A-102 $12.00 A-103 $8.00] [order A-101: bad amount "pending"]
func (Try[T]) MapErr ¶
MapErr yields f applied to each successful element; a failed call yields (zero, err); errored elements pass through.
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// Parse, then validate: two stages that can each fail. Errors from
// either one flow on side by side, in input order.
ports := catena.Of("8080", "https", "70000", "9090").MapErr(strconv.Atoi).
MapErr(func(port int) (int, error) {
if port > 65535 {
return 0, fmt.Errorf("port %d out of range", port)
}
return port, nil
})
valid, errs := ports.CollectAll()
fmt.Println(valid)
fmt.Println(errs[0])
fmt.Println(errs[1])
}
Output: [8080 9090] strconv.Atoi: parsing "https": invalid syntax port 70000 out of range
func (Try[T]) Must ¶
Must yields the successful elements and panics with the error value on the first error — recover() receives the error itself.
Example ¶
package main
import (
"fmt"
"time"
"github.com/NerdMeNot/catena"
)
func main() {
// For input you wrote yourself — a retry schedule compiled into the
// binary — where a failure is a programming bug. The panic value is
// the error itself, so recover() can inspect it.
defer func() { fmt.Println("recovered:", recover()) }()
backoff := catena.Of("1s", "5s", "30sec").MapErr(time.ParseDuration)
fmt.Println(backoff.Must().Collect())
}
Output: recovered: time: unknown unit "sec" in duration "30sec"
func (Try[T]) OnEach ¶
OnEach calls f on every successful element and passes everything through.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// A tap for the rows that worked — here a running total for the import
// summary — without ending the pipeline. Failed rows pass by untouched.
importedCents := 0
_, errs := importedOrders().
OnEach(func(o importedOrder) { importedCents += o.Cents }).
CollectAll()
fmt.Println(importedCents, len(errs))
}
Output: 4500 1
func (Try[T]) OnError ¶
OnError calls f on every error and passes everything through — a logging hook.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// The logging tap, the mirror of OnEach: record each failure as it
// goes by and still choose afterwards what failures mean. Here they
// are logged, then skipped.
orders := importedOrders().
OnError(func(err error) { fmt.Println("skipped:", err) }).
Ignore().
Collect()
fmt.Println(len(orders), "orders imported")
}
Output: skipped: order A-101: bad amount "pending" 3 orders imported
func (Try[T]) Pull ¶
Pull converts t to a pull-based iterator. THE CALLER MUST CALL stop, even if next has returned false, or resources held by t will leak.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// When the consumer has to drive — one record at a time, between
// other work. A failed element still arrives with ok true; ok is false
// only at the end. Ownership transfers to you: always call stop.
next, stop := importedOrders().Pull()
defer stop()
order, err, ok := next()
fmt.Println(order, err, ok)
_, err, ok = next()
fmt.Println(err, ok)
}
Output: {A-100 2500} <nil> true order A-101: bad amount "pending" true
func (Try[T]) Recover ¶
Recover offers each error to f: reporting true replaces the element with (v, nil); reporting false passes the error through unchanged.
Example ¶
package main
import (
"errors"
"fmt"
"time"
"github.com/NerdMeNot/catena"
)
func main() {
// Fall back where a sensible default exists and let the rest through:
// an unset timeout becomes 30s, a malformed one is still an error.
// Reporting true replaces the element, false lets the error continue.
errUnset := errors.New("not set")
parse := func(s string) (time.Duration, error) {
if s == "" {
return 0, errUnset
}
return time.ParseDuration(s)
}
timeouts := catena.Of("5s", "", "fast").MapErr(parse).
Recover(func(err error) (time.Duration, bool) {
return 30 * time.Second, errors.Is(err, errUnset)
})
vals, errs := timeouts.CollectAll()
fmt.Println(vals, errs)
}
Output: [5s 30s] [time: invalid duration "fast"]
func (Try[T]) Seq2 ¶
Seq2 converts to the stdlib iterator type. Free.
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// A free conversion to the standard pair iterator, for a plain range
// loop that handles each failure its own way.
ports := catena.Of("8080", "https", "9090").MapErr(strconv.Atoi)
for port, err := range ports.Seq2() {
if err != nil {
fmt.Println("skipping:", err)
continue
}
fmt.Println("listening on", port)
}
}
Output: listening on 8080 skipping: strconv.Atoi: parsing "https": invalid syntax listening on 9090
func (Try[T]) Take ¶
Take yields at most the first n elements, errored or not (R2). Panics if n is negative.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// A preview of the first two rows is two rows, bad ones included —
// Take counts elements, so it never reads past n looking for
// successes. For "the first two that parsed", Ignore first.
preview, errs := importedOrders().Take(2).CollectAll()
firstTwoGood := importedOrders().Ignore().Take(2).Collect()
fmt.Println(preview, errs)
fmt.Println(firstTwoGood)
}
Output: [{A-100 2500}] [order A-101: bad amount "pending"] [{A-100 2500} {A-102 1200}]
func (Try[T]) TakeWhile ¶
TakeWhile yields elements until pred rejects a successful element; errored elements pass through and do not terminate (R3).
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// Latency samples up to the first slow one. The dropped sample "--"
// passes through without ending the run; only a successful element
// failing the predicate stops it.
samples := catena.Of("12", "15", "--", "18", "950", "14").MapErr(strconv.Atoi)
fast, errs := samples.TakeWhile(func(ms int) bool { return ms < 100 }).CollectAll()
fmt.Println(fast, len(errs))
}
Output: [12 15 18] 1
func (Try[T]) Then ¶ added in v1.6.0
Then returns f(t): Seq.Then for a sequence that carries errors. It lets a function taking a Try sit inside a chain — chiefly the parallel package's Try stages, as in t.Then(parallel.MapTry(ctx, 8, enrich)), so a pipeline can go concurrent without first deciding what its errors mean. A function needing more arguments goes in a closure.
Example ¶
package main
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
"github.com/NerdMeNot/catena/parallel"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// Then keeps a Try in the chain through a function that takes one —
// here a concurrent stage, so the orders are confirmed four at a time
// without first deciding what the import's errors mean. The bad row
// is never sent to the payment service; it arrives in its place.
confirm := func(ctx context.Context, o importedOrder) (string, error) {
return fmt.Sprintf("%s confirmed", o.ID), nil
}
confirmed, failed := importedOrders().
Then(parallel.MapTry(context.Background(), 4, confirm)).
CollectAll()
fmt.Println(confirmed)
fmt.Println(failed)
}
Output: [A-100 confirmed A-102 confirmed A-103 confirmed] [order A-101: bad amount "pending"]
func (Try[T]) UntilDone ¶
UntilDone passes elements through until ctx is done, then yields (zero, ctx.Err()) and stops.
Example ¶
package main
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// Cancellation enters at the edge. Once ctx is done the sequence
// yields ctx's error and stops, so a shutdown surfaces like any other
// failure. Here the request is cancelled while the first order saves.
ctx, cancel := context.WithCancel(context.Background())
saved, err := importedOrders().UntilDone(ctx).
OnEach(func(importedOrder) { cancel() }).
Collect()
fmt.Println(len(saved), err)
}
Output: 1 context canceled
func (Try[T]) UntilErr ¶ added in v1.5.0
UntilErr yields the successful elements up to the first error, then stops and stores that error in *err: the policy Collect applies, as a sequence that keeps chaining. *err is set to nil when an iteration begins, so once the pipeline has been consumed it holds that run's first error, or nil — check it then, the way bufio.Scanner's Err is checked after the loop. Ignore keeps going past errors and Must panics on one; this is the exit that stops quietly and tells you why.
Example ¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/NerdMeNot/catena"
)
// importedOrder is one row of the order export the Try examples share.
type importedOrder struct {
ID string
Cents int
}
// importedOrders parses a four-row CSV export whose second row has a bad
// amount: three orders that parse and one that fails, so every operator's
// treatment of an error is visible.
func importedOrders() catena.Try[importedOrder] {
rows := catena.Of("A-100,2500", "A-101,pending", "A-102,1200", "A-103,800")
return rows.MapErr(func(row string) (importedOrder, error) {
id, amount, _ := strings.Cut(row, ",")
cents, err := strconv.Atoi(amount)
if err != nil {
return importedOrder{}, fmt.Errorf("order %s: bad amount %q", id, amount)
}
return importedOrder{ID: id, Cents: cents}, nil
})
}
func main() {
// Stop at the first failure, but as a sequence: the chain goes on
// with the full Seq API, and the error is checked once it has run —
// the way bufio.Scanner's Err is read after the loop.
var err error
revenue := importedOrders().UntilErr(&err).
Map(func(o importedOrder) int { return o.Cents }).
Then(catena.Sum)
if err != nil {
fmt.Println("import stopped:", err)
}
fmt.Println("revenue so far:", revenue)
}
Output: import stopped: order A-101: bad amount "pending" revenue so far: 2500
func (Try[T]) WrapErr ¶
WrapErr replaces each error with f(err) — the place to add positional context. If f returns nil (a caller bug), the original error is kept: an error is never converted into a zero-value success.
Example ¶
package main
import (
"fmt"
"strconv"
"github.com/NerdMeNot/catena"
)
func main() {
// The parser knows what went wrong; only this stage knows where. Add
// that here. Returning nil keeps the original error rather than
// turning a failure into a zero value.
line := 0
quantities := catena.Of("12", "n/a", "7").
MapErr(func(field string) (int, error) {
line++
return strconv.Atoi(field)
}).
WrapErr(func(err error) error {
return fmt.Errorf("stock.csv line %d: %w", line, err)
})
_, err := quantities.Collect()
fmt.Println(err)
}
Output: stock.csv line 2: strconv.Atoi: parsing "n/a": invalid syntax
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
01-basics
command
Basics: building a pipeline, what laziness means in practice, and how catena sequences interoperate with plain range loops and iter.Seq.
|
Basics: building a pipeline, what laziness means in practice, and how catena sequences interoperate with plain range loops and iter.Seq. |
|
02-grouping
command
Grouping: FoldBy is the library's centerpiece — streaming aggregation bounded by distinct keys, not elements.
|
Grouping: FoldBy is the library's centerpiece — streaming aggregation bounded by distinct keys, not elements. |
|
03-selection
command
Selection: finding extremes and top-k without sorting the world.
|
Selection: finding extremes and top-k without sorting the world. |
|
04-errors
command
Errors: one parse pipeline, three policies.
|
Errors: one parse pipeline, three policies. |
|
05-resources
command
Resources: a producer that owns something — here a real temp file — opens it lazily inside the iteration closure.
|
Resources: a producer that owns something — here a real temp file — opens it lazily inside the iteration closure. |
|
06-streaming
command
Streaming: sequences with no end, and the operators that stay safe on them — running state with Scan, moving averages with Windowed, batching with Chunked, and change detection with DedupeBy.
|
Streaming: sequences with no end, and the operators that stay safe on them — running state with Scan, moving averages with Windowed, batching with Chunked, and change detection with DedupeBy. |
|
07-join
command
Join: JoinBy is a relational inner join between two streams of structs — the right side is indexed by key, the left side streams past it, and the joined stream is a plain Seq you keep chaining on.
|
Join: JoinBy is a relational inner join between two streams of structs — the right side is indexed by key, the left side streams past it, and the joined stream is a plain Seq you keep chaining on. |
|
08-list
command
List and the Seq2 bridge: when the data is small and already in memory, eager evaluation with exact preallocation beats a lazy chain — and the two worlds convert explicitly, in both directions.
|
List and the Seq2 bridge: when the data is small and already in memory, eager evaluation with exact preallocation beats a lazy chain — and the two worlds convert explicitly, in both directions. |
|
09-concurrency
command
Concurrency: a nightly job that turns a batch of orders into a shipping report, where every order needs two calls to other services.
|
Concurrency: a nightly job that turns a batch of orders into a shipping report, where every order needs two calls to other services. |
|
10-sources
command
Sources: the producers in catena/source open what they read inside the iteration, so a pipeline that is built holds nothing, and a pipeline that stops early releases what it opened.
|
Sources: the producers in catena/source open what they read inside the iteration, so a pipeline that is built holds nothing, and a pipeline that stops early releases what it opened. |
|
internal
|
|
|
gen/assets
command
Command assets generates the catena mark as SVG.
|
Command assets generates the catena mark as SVG. |
|
gen/listgen
command
Command listgen generates list_gen.go: the eager List[T] mirror of the Seq[T] operation set.
|
Command listgen generates list_gen.go: the eager List[T] mirror of the Seq[T] operation set. |
|
gen/opdocs
command
Command opdocs generates the operator reference in docs/operators/.
|
Command opdocs generates the operator reference in docs/operators/. |
|
lint
module
|
|
|
Package parallel holds catena's concurrency and time operators: the ones that start goroutines, which the core package never does.
|
Package parallel holds catena's concurrency and time operators: the ones that start goroutines, which the core package never does. |
|
Package source holds ready-made producers for the things pipelines most often start from: the lines of a reader, CSV records with or without a header, JSON Lines, and the pages of a paginated API.
|
Package source holds ready-made producers for the things pipelines most often start from: the lines of a reader, CSV records with or without a header, JSON Lines, and the pages of a paginated API. |
|
sqlsource
Package sqlsource is the database/sql producer of catena/source, in a package of its own so that the others do not link database/sql.
|
Package sqlsource is the database/sql producer of catena/source, in a package of its own so that the others do not link database/sql. |