Skip to content
catena
v1.1.0
Theme

This content is for v1.1.0. Switch to the latest version for up-to-date documentation.

Getting started

catena needs Go 1.27 or later — the chainable API is built on generic methods, which arrived in 1.27. There are no dependencies.

Terminal window
go get github.com/NerdMeNot/catena
import "github.com/NerdMeNot/catena"
evens := catena.Range(0, 100, 1).
Filter(func(n int) bool { return n%2 == 0 }).
Map(func(n int) int { return n * n }).
Take(5).
Collect()
// [0 4 16 36 64]

Everything before Collect is lazy: nothing runs until a terminal consumes the sequence, and Take(5) means only five squares are ever computed — even though the range describes a hundred.

Seq is Go’s iter.Seq underneath, so range works directly and nothing needs converting at either edge:

for v := range catena.FromSlice(xs).Filter(pred) {
...
}
s := catena.From(maps.Keys(m)) // any iter.Seq adapts for free
std := s.Seq() // and converts back for free

Grouped aggregation without the intermediate map

Section titled “Grouped aggregation without the intermediate map”

The operator to learn first is FoldBy: it folds each element into a per-key accumulator as elements stream past. GroupBy retains every element; FoldBy retains one accumulator per distinct key.

totals := catena.FromSlice(orders).FoldBy(
func(o Order) string { return o.Customer }, // key
func(string) int { return 0 }, // initial accumulator
func(sum int, o Order) int { return sum + o.Amount },
)

Its companion is TopNBy, which keeps a bounded heap of n instead of sorting everything:

top := catena.FromSlice(orders).TopNBy(10, func(o Order) int { return o.Amount })

On a large scan, these two are the difference between a working pipeline and an out-of-memory kill — measured numbers in 05-performance.md.

Parsing, scanning, and I/O produce a Try[T] — a sequence where each element either succeeded or carries an error. The pipeline doesn’t decide what an error means; the consumer does:

nums := catena.FromSlice(lines).MapErr(strconv.Atoi)
vals, err := nums.Collect() // stop at the first bad line
vals, errs := nums.CollectAll() // keep everything, gather all errors
vals := nums.Ignore().Collect() // skip bad lines, keep going

The full story — including how to write producers for files and sql.Rows that clean up after themselves — is in 03-error-handling.md.

List[T] is a []T with the same operation set, evaluated eagerly with exact preallocation. Use Seq for I/O, large scans, and anything short-circuiting; use List for small collections you touch more than once. Crossing between them is always explicit: l.AsSeq() and s.ToList(). If the eager/lazy distinction is new, the concepts doc walks through what actually runs when, and when eager is the better choice.

  1. Treat every Seq as single-pass. Whether re-iteration works depends on the producer; slices and ranges are safe, channels and rows are not. Once() turns a double consume into a loud panic during development.
  2. Nil is empty. A nil Seq behaves as an empty sequence everywhere — no nil checks needed at call sites.
  3. Bad arguments panic at construction, with a catena: prefix, at the line that made the mistake — not three files later inside a range loop. Zero counts are valid values, never panics.

The rest of the contract system is in 02-concepts.md.