Consuming
The operators that make a pipeline run. Everything before one of these is a description of work; one of these performs it, once. Collect and ForEach drain the sequence, so neither returns on an infinite one — Pull and ToChan hand control back to you instead, and both make you responsible for stopping.
Collect
Section titled “Collect”func (s Seq[T]) Collect() []TCollect drains the sequence into a slice; nil for empty. ToList is the same drain returning a List, which carries the eager operator set.
// 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)[web-1:443] trueToList
Section titled “ToList”func (s Seq[T]) ToList() List[T]ToList drains the sequence into a List; nil for empty.
// 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))3 v1.5.0func (s Seq[T]) Seq() iter.Seq[T]Seq converts to the stdlib iterator type. Free.
// 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()))[db-1 web-1 web-2]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.
// 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.next, stop := rows.Pull()defer stop()
header, _ := next()record, ok := next()fmt.Println(header, "|", record, ok)id,email | 1,[email protected] trueToChan
Section titled “ToChan”func (s Seq[T]) ToChan(ctx context.Context) <-chan TToChan 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.
// 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.Int32var workers sync.WaitGroupfor range 2 { workers.Go(func() { for range jobs { resized.Add(1) } })}workers.Wait()fmt.Println(resized.Load())4ForEach
Section titled “ForEach”func (s Seq[T]) ForEach(f func(T))ForEach calls f on every element.
// 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) })FAIL pkg/dbFAIL pkg/authForEachIndexed
Section titled “ForEachIndexed”func (s Seq[T]) ForEachIndexed(f func(int, T))ForEachIndexed calls f(index, element) on every element.
// 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)})1. back up the database2. run migrations3. restart the APIForEachErr
Section titled “ForEachErr”func (s Seq[T]) ForEachErr(f func(T) error) errorForEachErr calls f on every element, stopping at and returning the first non-nil error; nil if the sequence drains clean.
// 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)charged A-1err: order A-2: no card on filefunc (s Seq[T]) Drain()Drain consumes the sequence for its side effects.
// 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])2 bobfunc (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.).
// 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()3 linesrecovered: catena: Once: sequence consumed more than onceUntilDone
Section titled “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.
// 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)[1 2 3] context canceled