catena

package module
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

catena

Say what you want from the data. Skip the loop. Chained, fully typed, lazy pipelines for Go — the grouping loop, the worker pool and the error plumbing, already written and tested harder than you would test your own.

CI Release Go Reference

// The ten largest overdue invoices. One pass; ten rows in memory, not the table.
overdue := catena.FromSlice(invoices).
    FilterNot(Invoice.IsPaid).
    Filter(func(i Invoice) bool { return i.Due.Before(today) }).
    TopNBy(10, Invoice.Amount)
go get github.com/NerdMeNot/catena

Requires Go 1.27+ for generic methods. Adding catena therefore raises your module's own go directive to 1.27, and everyone building your code — teammates and CI included — needs a 1.27 toolchain. No runtime dependencies: the one require line is a test-only property-testing library, and go list -deps on the package and its sub-packages resolves to the standard library alone.

Catena is Latin for "chain".


Three things you stop writing

The grouping loop — the map, the slice of keys so the output has an order, the if _, ok, the sort at the end:

// Revenue per country, largest first. Memory is bounded by the number of
// countries, not the number of orders.
ranked := catena.FromSlice(orders).
    FoldedBy(Order.Country,
        func(string) int { return 0 },
        func(sum int, o Order) int { return sum + o.Total }).
    MapTo(func(country string, total int) Row { return Row{country, total} }).
    SortedByDesc(func(r Row) int { return r.Total })

The worker pool — the semaphore, the WaitGroup, the results slice indexed so things come back in order, the error channel, cancelling the rest when one fails:

// func loadCustomer(ctx context.Context, id ID) (Customer, error)

customers, err := catena.FromSlice(ids).
    Then(parallel.Map(ctx, 8, loadCustomer)). // 8 in flight, results in order
    Collect()                                 // first error cancels the rest

loadCustomer is an ordinary function. Every goroutine has exited by the time Collect returns — on success, on error, on cancel and on panic.

The error plumbing — the if err != nil on every other line, and the decision about what a bad row means buried where the caller cannot change it:

rows := source.Lines(open).MapErr(parseRow) // a failure is an element

good, err  := rows.Collect()          // stop at the first bad row
good, errs := rows.CollectAll()       // import what parses, report the rest
good       := rows.Ignore().Collect() // skip bad rows

The file is opened when the pipeline runs, and closed on every way out of it.

Callbacks do not have to be closures: a method expression such as Invoice.IsPaid or Order.Country is already a function of its receiver, and goes wherever a callback does.


Why

Go finally has the two pieces this needed — iter.Seq as a common iterator currency, and generic methods so a chain can change element type mid-flight. What was missing was a library willing to be precise about the things that make or break iterator code in practice.

Nothing is hidden. No reflect, no any in value position, no hidden buffering, and exactly one goroutine in the core package — ToChan's, declared in the first line of its doc comment. Concurrency is a separate import, catena/parallel, where every operator joins the goroutines it started before it returns. An operator that buffers says so too, with its bound; a terminal that would hang on an infinite sequence is marked; an invalid argument panics at construction, naming the operator you actually called. The only panics during iteration are Once's and Must's, which is what those two are for.

The contracts are tested, not promised. Every exported operator is registered in a conformance harness, compared against a hand-written loop, and put through the invariants its class allows: re-iteration, early termination over an infinite source, the producer's cleanup running on an early break, and a bound on how much of the source it may consume to produce k elements. A completeness check parses the package and fails CI for any operator nothing registers. Library statement coverage is 100%, and the badge is not a snapshot: CI recomputes it on every push and fails the build below 100%, so a regression turns the build red rather than the badge stale.

Slow only where Go is slow. Measured against the same pipelines hand-built from raw iter.Seq closures, catena beats the raw mechanism on multi-stage pipelines and on terminals, and costs 4% more on single-stage shapes — where the baseline is inlined into its consumer and a call across a package boundary cannot be. What remains against a plain loop is the iterator protocol itself, and every number is published rather than waved at.

In practice

Pipelines stay lazy until a terminal consumes them, and early termination propagates through every stage — Take(2) here computes exactly two squares, out of an infinite sequence:

catena.Generate(1, func(n int) int { return n * 2 }). // 1, 2, 4, 8, ...
    Map(func(n int) int { return n * n }).
    Take(2).
    Collect() // [1 4]

When elements can fail, the pipeline carries the errors and the consumer picks the policy — abort, gather, or skip:

nums := catena.FromSlice(lines).MapErr(strconv.Atoi)

// pick one:
vals, err   := nums.Collect()          // stop at the first bad line
vals, errs  := nums.CollectAll()       // keep everything, report everything
vals        := nums.Ignore().Collect() // skip bad lines

Producers that own a resource open it lazily inside the pipeline, so an unconsumed pipeline holds nothing and a downstream Take(3) still closes the rows — proven against the real database/sql stack. The ones you would otherwise write yourself ship in catena/source: Lines, CSV, JSONLines, Paginate, and sqlsource.Rows.

Callbacks that are slow run concurrently, without the pipeline changing shape. Then puts a stage in the chain; errors stay per-element, and nothing outlives the call:

bodies, err := catena.FromSlice(urls).
    Filter(isHTTPS).
    Then(parallel.Map(ctx, 8, fetch)). // eight at a time, results in order
    Collect()                     // every goroutine has exited by here

The operators worth switching for

Not syntax sugar — these change the memory class of the computation.

Use Instead of And it costs
FoldBy GroupBy + fold per bucket 2.97 MB → 1.2 KB (bounded by keys, not elements)
TopNBy(10, sel) SortedDesc().Take(10) 1.7 MB → 1 KB, 32× faster (bounded heap)
DedupeBy DistinctBy on sorted input unbounded seen-set → O(1)
Sample(10, rng) collecting everything to shuffle it 803 KB → 456 B, 4.4× faster (reservoir)

catenalint finds the left-hand column in your code and rewrites it where the result is provably the same.

Performance, before you choose

Measured three ways — a hand-written loop, the same pipeline hand-built from raw iter.Seq closures, and catena (Apple M5 Max, Go 1.27, 100k elements, bench_baseline_test.go):

Shape Hand loop Raw closures catena
4-stage pipeline (filter→map→filter→sum) 25µs 353µs 261µs
Sum 24µs 178µs 24µs
Contains — 130µs 24µs

Two facts fall out of that. catena adds nothing on top of Go's iterator mechanism — on these three shapes it beats the raw-closure baseline outright, and across the full table its worst single-stage showing is 4% behind, where the baseline inlines into its caller and a library cannot. A terminal straight over a source, like Sum over a slice, the compiler flattens into the hand loop itself. And a chain of stages has a real cost: against a plain loop doing trivial arithmetic, the four-stage pipeline runs about 10× slower — roughly 2.4ns per element across the whole chain, in closure calls the compiler cannot flatten. If your loop body does anything beyond arithmetic — parses, allocates, touches a map or a socket — the per-element work buries it and the two converge.

The full benchmarks show the code both ways, so you can judge what the nanoseconds buy.

The shape of the library

Type What it is
Seq[T] lazy iter.Seq[T] with 94 methods — 41 chainable, 53 terminal; range works directly
Seq2[K, V] the stdlib pair currency; a bridge back to Seq, not a peer
Try[T] iter.Seq2[T, error]; error policy chosen by the consumer
List[T] eager []T with the mirrored operation set — generated, and conformance-checked to agree with AsSeq()
Package What it adds
catena/parallel the operators that start goroutines: Map, MapUnordered, ForEach and their Try forms, Do, Buffer, ChunkedWithin, Merge — and Retry and Limit to wrap a callback
catena/source producers that open lazily and always release: Lines, CSV, JSONLines, Paginate, and sqlsource.Rows
catena/lint a separate module: catenalint, which finds pipelines that sort everything to keep ten, or never run

Interop is one conversion and zero cost, in both directions: slices.Sorted(s.Seq()) to hand a pipeline to the stdlib, catena.From(maps.Keys(m)) to take one back. Operations that constrain the element type (Distinct, Sorted, Sum, Max, …) 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] and would be an instantiation cycle as methods. The chain continues normally after either kind, and Then keeps it unbroken: s.Then(catena.Distinct).Then(catena.Sorted).Take(10).

Documentation

catena.nerdmenot.in — the documentation site: every guide and the full operator reference, versioned per release.

Operator reference — all 215 operators across 16 pages, each with its signature, its memory and termination behaviour, and a worked example that go test runs and verifies.

Guides, in reading order:

  1. Getting started — the five-minute tour
  2. Concepts — the four types, eager vs lazy, and the contract system
  3. Error handling — Try and resource-owning producers
  4. Operator catalog — the index, with memory and drain classes
  5. Performance — the three-way benchmarks, honestly
  6. Concurrency — the parallel package and its guarantee
  7. Linting — catenalint and what its fixes promise
  8. Coming from lo, Kotlin or LINQ — the name you know, and catena's
  9. Writing your own source — for what catena does not ship
  10. Releasing — how a release is cut and verified

Also: runnable examples · API reference

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

Examples

Constants

View Source
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

func Average[T Numeric](s Seq[T]) (float64, bool)

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

func BottomN[T cmp.Ordered](s Seq[T], n int) []T

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

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

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

func Join(s Seq[string], sep string) string

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

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

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

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

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

func MinMax[T cmp.Ordered](s Seq[T]) (min, max T, ok bool)

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

func Product[T Numeric](s Seq[T]) T

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

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

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

func TopN[T cmp.Ordered](s Seq[T], n int) []T

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

func Unzip[K, V any](s Seq2[K, V]) ([]K, []V)

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]) All

func (l List[T]) All(pred func(T) bool) bool

All mirrors Seq.All eagerly.

func (List[T]) Any

func (l List[T]) Any(pred func(T) bool) bool

Any mirrors Seq.Any eagerly.

func (List[T]) Append

func (l List[T]) Append(vals ...T) List[T]

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

func (l List[T]) AsSeq() Seq[T]

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

func (l List[T]) At(i int) T

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]) AverageOf

func (l List[T]) AverageOf[N Numeric](sel func(T) N) (float64, bool)

AverageOf mirrors Seq.AverageOf eagerly.

func (List[T]) BottomNBy

func (l List[T]) BottomNBy[K cmp.Ordered](n int, sel func(T) K) []T

BottomNBy mirrors Seq.BottomNBy eagerly.

func (List[T]) Clone

func (l List[T]) Clone() List[T]

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]) Concat

func (l List[T]) Concat(others ...Seq[T]) List[T]

Concat mirrors Seq.Concat eagerly.

func (List[T]) Count

func (l List[T]) Count() int

Count mirrors Seq.Count eagerly, implemented directly over the backing slice rather than through the lazy pipeline.

func (List[T]) CountWhere

func (l List[T]) CountWhere(pred func(T) bool) int

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

func (l List[T]) DistinctWith(eq func(a, b T) bool) List[T]

DistinctWith mirrors Seq.DistinctWith eagerly.

func (List[T]) Drain

func (l List[T]) Drain()

Drain mirrors Seq.Drain eagerly.

func (List[T]) Drop

func (l List[T]) Drop(n int) List[T]

Drop mirrors Seq.Drop eagerly.

func (List[T]) DropLast

func (l List[T]) DropLast(n int) List[T]

DropLast mirrors Seq.DropLast eagerly.

func (List[T]) DropWhile

func (l List[T]) DropWhile(pred func(T) bool) List[T]

DropWhile mirrors Seq.DropWhile 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

func (l List[T]) ElementAt(i int) (T, bool)

ElementAt mirrors Seq.ElementAt eagerly, implemented directly over the backing slice rather than through the lazy pipeline.

func (List[T]) Filter

func (l List[T]) Filter(pred func(T) bool) List[T]

Filter mirrors Seq.Filter eagerly.

func (List[T]) FilterErr

func (l List[T]) FilterErr(pred func(T) (bool, error)) Try[T]

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

func (l List[T]) FilterIndexed(pred func(int, T) bool) List[T]

FilterIndexed mirrors Seq.FilterIndexed eagerly.

func (List[T]) FilterMap

func (l List[T]) FilterMap[U any](f func(T) (U, bool)) List[U]

FilterMap mirrors Seq.FilterMap eagerly.

func (List[T]) FilterNot

func (l List[T]) FilterNot(pred func(T) bool) List[T]

FilterNot mirrors Seq.FilterNot eagerly.

func (List[T]) Find

func (l List[T]) Find(pred func(T) bool) (T, bool)

Find mirrors Seq.Find eagerly.

func (List[T]) FindIndex

func (l List[T]) FindIndex(pred func(T) bool) int

FindIndex mirrors Seq.FindIndex eagerly.

func (List[T]) FindLast

func (l List[T]) FindLast(pred func(T) bool) (T, bool)

FindLast mirrors Seq.FindLast eagerly.

func (List[T]) FindMap

func (l List[T]) FindMap[U any](f func(T) (U, bool)) (U, bool)

FindMap mirrors Seq.FindMap eagerly.

func (List[T]) First

func (l List[T]) First() (T, bool)

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]) FlatMap

func (l List[T]) FlatMap[U any](f func(T) Seq[U]) List[U]

FlatMap mirrors Seq.FlatMap eagerly.

func (List[T]) FlatMapSlice

func (l List[T]) FlatMapSlice[U any](f func(T) []U) List[U]

FlatMapSlice mirrors Seq.FlatMapSlice eagerly.

func (List[T]) Fold

func (l List[T]) Fold[A any](init A, f func(A, T) A) A

Fold mirrors Seq.Fold 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]) FoldErr

func (l List[T]) FoldErr[A any](init A, f func(A, T) (A, error)) (A, error)

FoldErr mirrors Seq.FoldErr eagerly.

func (List[T]) FoldIndexed

func (l List[T]) FoldIndexed[A any](init A, f func(int, A, T) A) A

FoldIndexed mirrors Seq.FoldIndexed eagerly.

func (List[T]) FoldRight

func (l List[T]) FoldRight[A any](init A, f func(T, A) A) A

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]) FoldWhile

func (l List[T]) FoldWhile[A any](init A, f func(A, T) (A, bool)) A

FoldWhile mirrors Seq.FoldWhile eagerly.

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]) ForEach

func (l List[T]) ForEach(f func(T))

ForEach mirrors Seq.ForEach eagerly.

func (List[T]) ForEachErr

func (l List[T]) ForEachErr(f func(T) error) error

ForEachErr mirrors Seq.ForEachErr eagerly.

func (List[T]) ForEachIndexed

func (l List[T]) ForEachIndexed(f func(int, T))

ForEachIndexed mirrors Seq.ForEachIndexed eagerly.

func (List[T]) Get

func (l List[T]) Get(i int) (T, bool)

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]) IfEmpty

func (l List[T]) IfEmpty(defaults ...T) List[T]

IfEmpty mirrors Seq.IfEmpty 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

func (l List[T]) Intersperse(sep T) List[T]

Intersperse mirrors Seq.Intersperse eagerly.

func (List[T]) IsEmpty

func (l List[T]) IsEmpty() bool

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

func (l List[T]) IsSortedBy[K cmp.Ordered](sel func(T) K) bool

IsSortedBy mirrors Seq.IsSortedBy eagerly.

func (List[T]) IsSortedWith added in v1.5.0

func (l List[T]) IsSortedWith(cmp func(a, b T) int) bool

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

func (l List[T]) JoinToString(sep string, sel func(T) string) string

JoinToString mirrors Seq.JoinToString eagerly.

func (List[T]) Last

func (l List[T]) Last() (T, bool)

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

func (l List[T]) Len() int

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

func (l List[T]) Map[U any](f func(T) U) List[U]

Map mirrors Seq.Map eagerly, implemented directly over the backing slice rather than through the lazy pipeline.

func (List[T]) MapErr

func (l List[T]) MapErr[U any](f func(T) (U, error)) Try[U]

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

func (l List[T]) MapIndexed[U any](f func(int, T) U) List[U]

MapIndexed mirrors Seq.MapIndexed eagerly, implemented directly over the backing slice rather than through the lazy pipeline.

func (List[T]) MaxBy

func (l List[T]) MaxBy[K cmp.Ordered](sel func(T) K) (T, bool)

MaxBy mirrors Seq.MaxBy eagerly.

func (List[T]) MaxOf

func (l List[T]) MaxOf[K cmp.Ordered](sel func(T) K) (K, bool)

MaxOf mirrors Seq.MaxOf eagerly.

func (List[T]) MaxWith

func (l List[T]) MaxWith(cmp func(a, b T) int) (T, bool)

MaxWith mirrors Seq.MaxWith eagerly.

func (List[T]) MinBy

func (l List[T]) MinBy[K cmp.Ordered](sel func(T) K) (T, bool)

MinBy mirrors Seq.MinBy eagerly.

func (List[T]) MinMaxOf

func (l List[T]) MinMaxOf[K cmp.Ordered](sel func(T) K) (min, max K, ok bool)

MinMaxOf mirrors Seq.MinMaxOf eagerly.

func (List[T]) MinOf

func (l List[T]) MinOf[K cmp.Ordered](sel func(T) K) (K, bool)

MinOf mirrors Seq.MinOf eagerly.

func (List[T]) MinWith

func (l List[T]) MinWith(cmp func(a, b T) int) (T, bool)

MinWith mirrors Seq.MinWith eagerly.

func (List[T]) None

func (l List[T]) None(pred func(T) bool) bool

None mirrors Seq.None eagerly.

func (List[T]) OnEach

func (l List[T]) OnEach(f func(T)) List[T]

OnEach mirrors Seq.OnEach eagerly.

func (List[T]) Partition

func (l List[T]) Partition(pred func(T) bool) (yes, no []T)

Partition mirrors Seq.Partition eagerly.

func (List[T]) Prepend

func (l List[T]) Prepend(vals ...T) List[T]

Prepend mirrors Seq.Prepend eagerly.

func (List[T]) ProductOf

func (l List[T]) ProductOf[N Numeric](sel func(T) N) N

ProductOf mirrors Seq.ProductOf eagerly.

func (List[T]) Reduce

func (l List[T]) Reduce(f func(T, T) T) (T, bool)

Reduce mirrors Seq.Reduce eagerly.

func (List[T]) Reversed

func (l List[T]) Reversed() List[T]

Reversed mirrors Seq.Reversed eagerly, implemented directly over the backing slice rather than through the lazy pipeline.

func (List[T]) Sample added in v1.5.0

func (l List[T]) Sample(n int, rng *rand.Rand) []T

Sample mirrors Seq.Sample eagerly.

func (List[T]) Scan

func (l List[T]) Scan[A any](init A, f func(A, T) A) List[A]

Scan mirrors Seq.Scan eagerly.

func (List[T]) Single

func (l List[T]) Single() (T, bool)

Single mirrors Seq.Single eagerly.

func (List[T]) Slice

func (l List[T]) Slice(i, j int) List[T]

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]) SortedBy

func (l List[T]) SortedBy[K cmp.Ordered](sel func(T) K) List[T]

SortedBy mirrors Seq.SortedBy eagerly.

func (List[T]) SortedByDesc

func (l List[T]) SortedByDesc[K cmp.Ordered](sel func(T) K) List[T]

SortedByDesc mirrors Seq.SortedByDesc eagerly.

func (List[T]) SortedWith

func (l List[T]) SortedWith(cmp func(a, b T) int) List[T]

SortedWith mirrors Seq.SortedWith eagerly.

func (List[T]) Step

func (l List[T]) Step(n int) List[T]

Step mirrors Seq.Step eagerly.

func (List[T]) SumOf

func (l List[T]) SumOf[N Numeric](sel func(T) N) N

SumOf mirrors Seq.SumOf eagerly.

func (List[T]) Take

func (l List[T]) Take(n int) List[T]

Take mirrors Seq.Take eagerly.

func (List[T]) TakeLast

func (l List[T]) TakeLast(n int) List[T]

TakeLast mirrors Seq.TakeLast eagerly.

func (List[T]) TakeWhile

func (l List[T]) TakeWhile(pred func(T) bool) List[T]

TakeWhile mirrors Seq.TakeWhile 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

func (l List[T]) ToList() List[T]

ToList mirrors Seq.ToList eagerly, implemented directly over the backing slice rather than through the lazy pipeline.

func (List[T]) TopNBy

func (l List[T]) TopNBy[K cmp.Ordered](n int, sel func(T) K) []T

TopNBy mirrors Seq.TopNBy eagerly.

func (List[T]) WithIndex

func (l List[T]) WithIndex() Seq2[int, T]

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

func (l List[T]) ZipWithNext() Seq2[T, T]

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

type Seq[T any] iter.Seq[T]

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

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

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

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

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

func Cycle[T any](s Seq[T]) Seq[T]

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

func Empty[T any]() Seq[T]

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

func Flatten[T any](s Seq[Seq[T]]) Seq[T]

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

func FlattenSlices[T any](s Seq[[]T]) Seq[T]

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

func From[T any](seq func(func(T) bool)) Seq[T]

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

func FromChan[T any](ctx context.Context, ch <-chan T) Seq[T]

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

func FromSlice[T any](s []T) Seq[T]

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

func Generate[T any](seed T, next func(T) T) Seq[T]

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

func GenerateWhile[T any](seed T, next func(T) (T, bool)) Seq[T]

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

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

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

func Once1[T any](v T) Seq[T]

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

func Range[I Integer](start, stop, step I) Seq[I]

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

func Repeat[T any](v T) Seq[T]

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

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

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

func Sorted[T cmp.Ordered](s Seq[T]) Seq[T]

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

func SortedDesc[T cmp.Ordered](s Seq[T]) Seq[T]

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

func Windowed[T any](s Seq[T], size, step int) Seq[[]T]

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

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

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

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

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

func (s Seq[T]) Append(vals ...T) Seq[T]

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

func (s Seq[T]) AverageOf[N Numeric](sel func(T) N) (float64, bool)

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

func (s Seq[T]) BottomNBy[K cmp.Ordered](n int, sel func(T) K) []T

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

func (s Seq[T]) Concat(others ...Seq[T]) Seq[T]

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

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

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

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

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

func (s Seq[T]) DistinctWith(eq func(a, b T) bool) Seq[T]

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

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

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

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

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

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

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

func (s Seq[T]) ElementAt(i int) (T, bool)

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

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

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

func (s Seq[T]) FilterErr(pred func(T) (bool, error)) Try[T]

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

func (s Seq[T]) FilterIndexed(pred func(int, T) bool) Seq[T]

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

func (s Seq[T]) FilterMap[U any](f func(T) (U, bool)) Seq[U]

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

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

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

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

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

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

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

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

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

func (s Seq[T]) FindMap[U any](f func(T) (U, bool)) (U, bool)

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

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

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

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

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

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

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

func (s Seq[T]) Fold[A any](init A, f func(A, T) A) A

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

func (s Seq[T]) FoldErr[A any](init A, f func(A, T) (A, error)) (A, error)

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

func (s Seq[T]) FoldIndexed[A any](init A, f func(int, A, T) A) A

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

func (s Seq[T]) FoldWhile[A any](init A, f func(A, T) (A, bool)) A

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

func (s Seq[T]) ForEachErr(f func(T) error) error

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

func (s Seq[T]) ForEachIndexed(f func(int, T))

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

func (s Seq[T]) IfEmpty(defaults ...T) Seq[T]

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

func (s Seq[T]) Intersperse(sep T) Seq[T]

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

func (s Seq[T]) IsEmpty() bool

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

func (s Seq[T]) IsSortedBy[K cmp.Ordered](sel func(T) K) bool

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

func (s Seq[T]) IsSortedWith(cmp func(a, b T) int) bool

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

func (s Seq[T]) JoinToString(sep string, sel func(T) string) string

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

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

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

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

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

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

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

func (s Seq[T]) MapIndexed[U any](f func(int, T) U) Seq[U]

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

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

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

func (s Seq[T]) MaxOf[K cmp.Ordered](sel func(T) K) (K, bool)

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

func (s Seq[T]) MaxWith(cmp func(a, b T) int) (T, bool)

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

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

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

func (s Seq[T]) MinMaxOf[K cmp.Ordered](sel func(T) K) (min, max K, ok bool)

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

func (s Seq[T]) MinOf[K cmp.Ordered](sel func(T) K) (K, bool)

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

func (s Seq[T]) MinWith(cmp func(a, b T) int) (T, bool)

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

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

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

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

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

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

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

func (s Seq[T]) Partition(pred func(T) bool) (yes, no []T)

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

func (s Seq[T]) Prepend(vals ...T) Seq[T]

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

func (s Seq[T]) ProductOf[N Numeric](sel func(T) N) N

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

func (s Seq[T]) Pull() (next func() (T, bool), stop func())

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

func (s Seq[T]) Reduce(f func(T, T) T) (T, bool)

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

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

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

func (s Seq[T]) Sample(n int, rng *rand.Rand) []T

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

func (s Seq[T]) Scan[A any](init A, f func(A, T) A) Seq[A]

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

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

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

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

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

func (s Seq[T]) SortedBy[K cmp.Ordered](sel func(T) K) Seq[T]

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

func (s Seq[T]) SortedByDesc[K cmp.Ordered](sel func(T) K) Seq[T]

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

func (s Seq[T]) SortedWith(cmp func(a, b T) int) Seq[T]

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

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

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

func (s Seq[T]) SumOf[N Numeric](sel func(T) N) N

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

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

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

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

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

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

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

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

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

func (s Seq[T]) ToChan(ctx context.Context) <-chan T

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

func (s Seq[T]) ToList() List[T]

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

func (s Seq[T]) TopNBy[K cmp.Ordered](n int, sel func(T) K) []T

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

func (s Seq[T]) UntilDone(ctx context.Context) Try[T]

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

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

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

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

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

func (s Seq[T]) ZipWithNext() Seq2[T, T]

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

type Seq2[K, V any] iter.Seq2[K, V]

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

func Empty2[K, V any]() Seq2[K, V]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

func (s Seq2[K, V]) First() (K, V, bool)

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

func (s Seq2[K, V]) Fold[A any](init A, f func(A, K, V) A) A

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

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

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

func (s Seq2[K, V]) Map[K2, V2 any](f func(K, V) (K2, V2)) Seq2[K2, V2]

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

func (s Seq2[K, V]) MapTo[U any](f func(K, V) U) Seq[U]

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

func (s Seq2[K, V]) MapValues[V2 any](f func(K, V) V2) Seq2[K, V2]

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

func (s Seq2[K, V]) Pull() (next func() (K, V, bool), stop func())

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

func (s Seq2[K, V]) Seq2() iter.Seq2[K, V]

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

func (s Seq2[K, V]) Swap() Seq2[V, K]

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

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

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

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

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

type Try[T any] iter.Seq2[T, error]

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

func EmptyTry[T any]() Try[T]

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

func FromErrs[T any](seq func(func(T, error) bool)) Try[T]

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

func (t Try[T]) Collect() ([]T, error)

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

func (t Try[T]) CollectAll() ([]T, []error)

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

func (t Try[T]) Count() (int, error)

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

func (t Try[T]) Drop(n int) Try[T]

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

func (t Try[T]) Err() error

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

func (t Try[T]) Errs() Seq[error]

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

func (t Try[T]) Filter(pred func(T) bool) Try[T]

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

func (t Try[T]) FilterErr(pred func(T) (bool, error)) Try[T]

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

func (t Try[T]) FlatMap[U any](f func(T) Try[U]) Try[U]

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

func (t Try[T]) Fold[A any](init A, f func(A, T) A) (A, error)

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

func (t Try[T]) ForEach(f func(T) error) error

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

func (t Try[T]) Ignore() Seq[T]

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

func (t Try[T]) Map[U any](f func(T) U) Try[U]

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

func (t Try[T]) MapErr[U any](f func(T) (U, error)) Try[U]

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

func (t Try[T]) Must() Seq[T]

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

func (t Try[T]) OnEach(f func(T)) Try[T]

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

func (t Try[T]) OnError(f func(error)) Try[T]

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

func (t Try[T]) Pull() (next func() (T, error, bool), stop func())

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

func (t Try[T]) Recover(f func(error) (T, bool)) Try[T]

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

func (t Try[T]) Seq2() iter.Seq2[T, error]

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

func (t Try[T]) Take(n int) Try[T]

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

func (t Try[T]) TakeWhile(pred func(T) bool) Try[T]

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

func (t Try[T]) Then[U any](f func(Try[T]) U) U

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

func (t Try[T]) UntilDone(ctx context.Context) Try[T]

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

func (t Try[T]) UntilErr(err *error) Seq[T]

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

func (t Try[T]) WrapErr(f func(error) error) Try[T]

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

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.

Jump to

Keyboard shortcuts

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