Changelog
All notable changes to catena are documented here. The format follows Keep a Changelog; versions follow semantic versioning.
[Unreleased]
Section titled “[Unreleased]”Documentation only — no change to the library’s API or behaviour.
Documentation
Section titled “Documentation”- The examples are real work now. Every operator’s example — the code
the reference pages and pkg.go.dev show — is rewritten around a
situation a reader will recognise: unpaid orders, access logs, config
ports, CSV imports, health checks. They were mostly
catena.Of(1, 2, 3). All are still verified bygo test. - The
parallelexamples make real requests. They run against an HTTP server started for the tests, so the calls being run concurrently are genuine: loading the products for a page, racing three mirrors and taking the first to answer, purging a cache across edge nodes, batching click events for a bulk insert. - The
concurrencyexample program is a nightly job that prepares shipments with two service calls per order, against a local HTTP server, including the case of running three different calls at once. - The landing page, the README and the getting-started guide open with
what the library replaces — the grouping loop, the worker pool, the
error plumbing — and the landing page now covers
parallel,sourceand the linter, which it predated. Two stale claims on it are corrected: that there are no goroutines anywhere, and a memory figure from before 1.4. - The guides point out that a method expression such as
Order.Countryis already a callback, which a test now pins down. - Examples are complete. In the
parallelexamples the function being run concurrently is written out in the example rather than named and left to the imagination, and operators can now carry several examples:Mapshows API calls, a link checker that keeps going past failures, and CPU work;ForEachshows running three different calls at once. Where an example in another package calls a shared helper, the reference shows the helper’s source beneath it. - The reference shows an operator’s cautions as one list instead of a stack of separate boxes, and the concurrency guide opens with a table of what the package is used for, each row linked to a runnable example.
1.5.0 — 2026-10-10
Section titled “1.5.0 — 2026-10-10”The largest release since the API froze: the places a chain had to stop are closed, and three packages now sit beside the core. Nothing existing changes behaviour.
This reverses three earlier decisions, on purpose. The original design ruled out parallel operators (“a different library”), capped the operator count, and kept everything in one package. Concurrency is still a different package — the core starts no goroutine it did not start before — but it is no longer someone else’s problem.
-
Seq.Thenapplies a function to the sequence, so a function that takes one sits inside the chain instead of wrapping it. Every one-argument package function fits, with its types inferred:s.Then(catena.Distinct).Then(catena.Sorted).Take(10). It is also how the stages incatena/parallelattach. -
Ordered, chainable grouping.
Seq.FoldedByandcatena.GroupedByareFoldByandGroupBythat yield aSeq2of pairs, keys in the order they were first seen, instead of returning a map: the result is deterministic and the chain keeps going.GroupedByis a package function becauseSeq2[K, []T]leads back toSeq[[]T], the same instantiation cycle that makesChunkedone. -
Joins that drop nothing.
Seq.LeftJoinBykeeps a left element with no match and tellscombineso;Seq.GroupJoinByyields one result per left element with all of its matches together. -
Five ideas from
lo, in catena’s shape.Seq.FirstOrandSeq.LastOrreturn a fallback when the sequence is empty, so a lookup can sit inside an expression.IsSorted,Seq.IsSortedByandSeq.IsSortedWithcheck order in one pass with no buffer, stopping at the first pair out of order.DuplicatesandSeq.DuplicatesByyield each repeated value once, at its second occurrence.Seq.Sampledrawsnelements uniformly at random in O(n) memory by reservoir sampling — 803 KB → 456 B against collecting 100k elements to shuffle them, and 4.4× faster. A seeded generator makes the choice repeatable.
-
Try.UntilErrstops at the first error, asCollectdoes, but as a sequence: the chain continues with the fullSeqAPI and the error is left where you asked, to be checked once the pipeline has run. It is the policy exitTrywas missing —Ignorekeeps going andMustpanics — and it is how a fallible source feeds a stage that takes a plain sequence, the concurrent ones included. -
catena/parallel: concurrency. Six operators, each a stage forThen:Map,MapUnorderedandForEachrun a callback up tonat a time. The source stays on the consumer’s goroutine; only the callback runs elsewhere. A failed call is aTryelement, soCollect,CollectAllandIgnorechoose the policy as they always have.Bufferreads ahead,ChunkedWithinbatches by size or by time, andMergeinterleaves several sequences by arrival; these run their sources on goroutines of their own.
Every one joins the goroutines it started before its iteration returns — on a normal end, an early break, a cancelled context and a panic alike. That is tested under
testing/synctest, in virtual time, against callbacks that are deliberately slow to stop. -
catena/source: producers that ship.Linesfor the lines of a reader, of any length;Paginatefor a paginated API, fetching a page only once the one before it has been yielded in full; andsqlsource.Rowsfor a query, in a package of its own so the others do not linkdatabase/sql. Each opens its resource inside the iteration and releases it on every exit — the pattern the error-handling guide taught, now code. -
catena/lint: a linter, as a separate module so the library’s own still depends on nothing.catenalintruns as ago vettool and reports the pipelines that are correct but wasteful —Sorted(s).Take(n)whereTopNholdsnelements,Sorted(s).First()whereMinholds one,Distinct(Sorted(s))whereDedupeholds none, aGroupByused only to count — and the lazy sequence that is built and never consumed. It offers a fix only where the replacement returns the same values of the same type. It carries the library’s version, under its own prefix:lint/v1.5.0. -
Guides for concurrency and linting, and a lookup from lo, Kotlin and LINQ names to catena’s, every entry of which a test checks against the library. Reference pages for the two new packages, and
concurrencyandsourcesexamples.
Documentation
Section titled “Documentation”- The
v1.4.xseries is archived at/1-4/, per the rule that a minor series is snapshotted when the next minor ships.
Changed
Section titled “Changed”-
Terminals and grouping operators are faster, some of them much. Every operator consumed its source through an adapter that made a nil sequence behave as an empty one, and that made every source an opaque function value. The call now goes through a method small enough to inline, so a source built in the calling function is one the compiler can see through. Measured interleaved against 1.4, over 100k elements:
SumandContains71µs → 24µs, the speed of a hand-written loop;FoldBy16% faster;GroupByandJoinBy10–13%; single-stageMap5%. Multi-stage pipelines are unchanged. No API or behaviour change. -
The published hand-loop baselines were too slow, and are corrected. They were written inline in the benchmark body, where nothing may be optimised away, which made a plain loop look up to four times slower than it is. Measured as functions of their own, the hand-written 4-stage pipeline takes 25µs, not 39µs — so a 4-stage catena pipeline is about 10× a hand loop on trivial arithmetic, not 6.6× as the README and the performance guide said. The library did not get slower; the earlier comparison flattered it.
-
The 100% coverage gate now covers
parallel,sourceandsqlsourceas well as the core package, and the linter’s module is gated to 100% separately. CI also runs the linter over the examples, which must come back clean. -
The
selectionexample’s leaderboard sorted every player to show four. The linter found it; it usesTopNBynow, with identical output. -
The
joinexample rolls up withFoldedByin place of collecting map keys and sorting them, and shows the two new joins. -
The operator reference is generated for the sub-packages too, and the README’s “exactly one goroutine” is now stated of the core package, which is where it is true.
1.4.0 — 2026-09-01
Section titled “1.4.0 — 2026-09-01”A performance release with no API change: the terminals that gather a slice stopped recopying what they had already gathered.
Changed
Section titled “Changed”-
The gathering terminals no longer grow a slice with
append. Its curve is the wrong shape at size: past 256 elements Go grows a slice by about a quarter, and every step reallocates and recopies everything gathered so far, so collecting 100k elements allocated 4.1 MB to produce 800 KB across 32 reallocations. Elements now go into fixed blocks that are never resized, then into one exact-sized slice.Measured against v1.3.0, interleaved over 18 samples a side to cancel drift:
Collectis 14% faster and allocates 59% less (3.91 MiB → 1.60 MiB), and the buffer behindSorted*the same 59% less, at 3% faster. Three more allocation events, 2.3 MB fewer bytes. Nothing else in the suite moved. The returned slice also has exact capacity now, whereappendleft up to 25% slack for the caller to retain.Applies to
Collect,ToList,Partition,Unzip,Try.Collect,Try.CollectAll, theSorted*buffer andCycle’s replay buffer. No API change, and no behaviour change beyond the capacity of the returned slice.
docs/05-performance.mdshowed a hand-written clone-and-sort but quoted numbers measured fromSortedDesc(s).Take(10)— catena’s own sort path, which pays for the pipeline too. Against the code actually printed,TopNByis 12× faster and 803 KB → 1 KB; against the catena spelling it is 32× faster and 1.7 MB → 1 KB. Both are now stated, and both are measured:BenchmarkSortedTakeN_Handis new so the printed code is covered by a benchmark rather than borrowing one.- The
SortedDesc(s).Take(10)memory figure was 4.1 MB and is now 1.7 MB, because that path shares the gathering change above.List.MapversusSeqmap-and-collect moves from 7× to 6× for the same reason — the thing it is compared against got faster.
Documentation
Section titled “Documentation”- The
v1.3.xseries is archived at/1-3/, per the rule that a minor series is snapshotted when the next minor ships.
1.3.0 — 2026-08-31
Section titled “1.3.0 — 2026-08-31”Documentation and site only — no change to the library’s API or behaviour. The headline is that the docs are now versioned: a reader pinned to an older release can read that release’s documentation instead of the current one silently replacing it.
- Versioned documentation. The site serves the current release at the
root and archived releases under their own prefix, with a switcher in
the header.
v1.1.0is archived at/1-1/, and its pages carry a notice saying so. The snapshot is taken from the tag rather than the working tree — the versioning plugin archives whatever is currently checked out, which would have filed the newer API under the older version’s name. - A 1280×640 social preview card, wired as
og:imageandtwitter:imageacross every page. Links to the site previously unfurled with no image at all. The same file is the repository’s GitHub social preview, which has no API and is uploaded by hand.
Changed
Section titled “Changed”- The documentation site is now published from released tags only. Its
content is derived from the tree it is built from while its version
label comes from the newest release, so publishing
mainput the two in disagreement the moment anything landed unreleased — the site briefly documentedBottomNwhile announcing itself as v1.1.0, which does not have it. A push tomainnow builds the site and runs every check against it without deploying. A site-only change can still be published from a branch, guarded by a check that refuses unlessdocs/operators/matches the latest release tag. - Canonical URLs, the sitemap and the
og:imagenow namecatena.nerdmenot.in. The site answers on both that and its Pages subdomain, and naming the subdomain made the branded domain the duplicate. - The version switcher is the site’s own component rather than the
plugin’s native
<select>, whose option list is drawn by the operating system and takes none of the site’s type or colour. Versions are links, so one can be opened in a new tab. APIandGitHubin the top navigation open in a new tab, on the landing page and in the docs header.- The README links the documentation site, which it never had.
- The theme toggle lost its selected state on documentation pages. It
renders twice there — the header and the mobile menu — and both named
their radios
catena-theme; radios sharing a name form one group however far apart they sit, so the browser kept only the last checked one. - Switcher links pointed at pages that need not exist in every version.
The 404 page’s switcher offered
/404/and/1-1/404/, neither a route. Links now fall back to a version’s front page where the same page is absent, and/1-1/itself is a page rather than a 404. scripts/sync.tscleared all ofsrc/content/docs/on every run, which would have destroyed the archived versions permanently — nothing can regenerate a snapshot of a tree that no longer exists. It now clears only the directories it generates.
Removed
Section titled “Removed”SPEC.md. The design document had become a second place where the library’s contracts were written down, and the two could disagree — as they did: itsTryrule set contradicted the code, its symbol counts had drifted, and it specified a conformance harness shape that does not exist. Each operator’s contract lives in its own doc comment, where godoc shows it and the conformance suite enforces it. The document remains in thev1.2.0tag for anyone who wants the rejected-alternatives history.
1.2.0 — 2026-08-31
Section titled “1.2.0 — 2026-08-31”One new operator and a substantial documentation pass. The pass came out of an ergonomics and cognitive-load audit whose finding was that the library’s most useful facts were written down where godoc discards them — so nothing about the API changed, but a good deal more of it is now visible from the place people read.
BottomN(s, n)— the no-selector form ofBottomNBy, completing the pair withTopN. Without it, wanting the smallest n meant eitherBottomNBy(n, catena.Self[T])or, more likely,Sorted(s).Take(n), which quietly gives up the O(n) memory bound that makes the operator worth having. Routes through the same bounded heap, with the same stability and tie rules.
- Four generated
Listmirrors documented themselves as evaluating “eagerly” while returning a lazy value:WithIndexandZipWithNextreturn aSeq2,MapErrandFilterErrreturn aTry. They now say so, and the claim in Concepts that no operator changes evaluation strategy names them as the exceptions. - The eager-operators page claimed
Listcarries “the wholeSeqoperation set”. It carries the whole method set: the constraint-bound package functions take aSeq, socatena.Sorted(l.AsSeq()).ToList()is the round-trip, andConcatneedsl2.AsSeq(). Both are now documented on theListtype and on the page. - The package doc pointed at a “constructor table” that did not exist in
godoc. The normative re-iterability table now lives on the
Seqtype, and the nine constructors that said nothing about re-iterability now state it — includingCycle, which is re-iterable iff its source’s first pass is.
Documentation
Section titled “Documentation”Try’s five error rules were cited by number eight times in godoc and defined nowhere in it: the block was a file-level comment, which godoc discards. R1–R5 now live on theTrytype, along with the rule thatTryis deliberately a small surface — carry errors one stage, then commit to a policy and continue onSeq. Same fix forSeq2’s “deliberately absent” list and thefuncs.goexplanation of why constraint-bound operations cannot be methods.- The method-versus-package-function split is now stated in the package
doc and as a fourth rule in Getting started. It is the first thing that
bites a newcomer, and the compiler cannot say it:
s.Distinct()produces an error indistinguishable from a typo. The-Bymethods an IDE does surface (SortedBy,DistinctBy,SumOf,MaxBy,TopNBy) now name their unsuffixed package-level siblings. - Added the re-iterability guarantee to Concepts: operators build state inside the returned closure, so a chain is re-iterable exactly when its root producer is. This turns an open-ended worry into one lookup.
- Added a cross-ecosystem name map (LINQ, Kotlin, Rust) to Getting
started, an index page for the operator reference, and mutual
cross-references for the confusable pairs —
MaxBy/MaxOf,Distinct/Dedupe,Collect/ToList,Chain/Concat,Once/Once1,JoinBy/Join. - The
[S]marker in the operator catalog read as a warning while certifying the opposite; it now says what it means. README notes that adopting catena raises a consumer’s owngodirective to 1.27, and its two non-compiling snippets are fixed. - The naming law gained the eight suffix patterns it did not cover, its
known exceptions (
AssociateWith,WithIndex), and the third reason an operation lands at package level (receiver shape).
1.1.0 — 2026-08-31
Section titled “1.1.0 — 2026-08-31”The first usable release. v1.0.0 was published briefly and withdrawn;
it is retracted in go.mod and should not be used. Start here.
The library
Section titled “The library”Seq[T]— lazyiter.Seq[T]with 84 methods (37 chainable), built on Go 1.27 generic methods;rangeworks directly and conversion to and from the standard iterator types is free.Try[T]— fallible sequences where the consumer chooses the error policy (Collectaborts,CollectAllgathers,Ignoreskips), under five uniform rules for how errors flow through intermediates.Seq2[K, V]— the stdlib pair bridge;List[T]— the eager mirror, generated and conformance-checked to agree withAsSeq().- The memory-class operators:
FoldBy(grouped aggregation bounded by keys, not elements),TopNBy/BottomNBy(bounded stable heap),DedupeBy(O(1) streaming distinct),ChunkedBy,Windowed,JoinBy(relational inner join). - Every operator’s contract — argument edges, memory class, drain class, ordering and tie policy, error semantics — specified in each operator’s doc comment and enforced by the C1–C15 conformance suite, a completeness check that fails CI for any unregistered export, property tests, and 100% statement coverage of the library.
- An operator reference covering all 182 operators, each with a worked
example that
go testruns and whose output it verifies. examples/— eight runnable programs, all executed in CI.- Performance measured three ways (hand loop / raw
iter.Seqclosures / catena) with the standing rule that catena meets the raw mechanism wherever the two compile on equal terms — it wins on multi-stage pipelines and terminals, and trails by 4–13% on single-stage shapes, where the baseline inlines into its consumer and a library call cannot. Operators compose by direct source invocation rather thanrange, which makes a 4-stage pipeline 25% faster than the same pipeline hand-built from raw closures.