English | 简体中文
Chainable, lazy collection pipelines for Go, built on iter.Seq.
seq is a generic Go library that wraps the standard library's lazy iterators iter.Seq / iter.Seq2 (Go 1.23+) and gives them Scala-style, left-to-right, chainable operations:
sum := seq.From([]int{1, 2, 3, 4, 5, 6}).
Filter(func(x int) bool { return x%2 == 0 }).
SumBy(func(x int) int { return x * x })
⚠️ This library requires Go 1.27. The chainable methods depend on the generic methods proposal (golang/go#77273), which has been accepted and is implemented in Go 1.27. A 1.27 toolchain (currentlygo1.27rc1) is required to build. See Status below.
Today, libraries like samber/lo can only offer top-level functions, so a pipeline reads inside-out:
// Reading order is the reverse of data flow: you start at the innermost Filter.
sum := lo.Sum(
lo.Map(
lo.Filter([]int{1, 2, 3, 4, 5, 6}, func(x int, _ int) bool { return x%2 == 0 }),
func(x int, _ int) int { return x * x },
),
)This isn't a style preference — it's a language limitation. Before Go 1.27, a method cannot declare its own type parameters, so a .Map() that turns Seq[int] into Seq[string] is impossible to express:
// Pre-1.27: this does not compile. Methods may not have their own [U any].
func (s Seq[T]) Map[U any](f func(T) U) Seq[U]golang/go#77273 lifts that restriction. Chainable, lazy, discoverable pipelines become possible — and that is the entire reason this library exists.
type Seq[T any] iter.Seq[T] // = func(yield func(T) bool)
type Seq2[K, V any] iter.Seq2[K, V]This gives zero-cost conversion: any iter.Seq[T] can be used directly as a Seq[T] and vice versa, and the result feeds straight into slices.Collect, maps.Keys, etc. The cost is that Seq[T] declares T as any at the type level — and that single fact decides half the API.
Because Seq[T any] pins T to any, a method's type parameters are fresh and independent — they cannot add a constraint back onto the receiver's T. So:
-
Operations that constrain
Titself must be free functions:func Distinct[T comparable](s Seq[T]) Seq[T] func Max[T cmp.Ordered](s Seq[T]) Optional[T] func Sum[T Numeric](s Seq[T]) T
-
Operations that only use a method's own constrained parameter can be methods (the "escape hatch"):
func (s Seq[T]) DistinctBy[K comparable](key func(T) K) Seq[T] func (s Seq[T]) GroupBy[K comparable](key func(T) K) map[K][]T
-
Operations over multiple generator types or a specific instantiation must be free functions:
func Zip[A, B any](a Seq[A], b Seq[B]) Seq2[A, B] // two independent types func Flatten[T any](s Seq[Seq[T]]) Seq[T] // receiver must be Seq[Seq[T]]
The rule is mechanically checkable: ask "does it constrain T itself?" If yes, it's a function; if no, it can be a method.
| Group | Form | Examples |
|---|---|---|
| Constructors | free functions | From, Of, Range, RangeStep, Repeat, Generate, Iterate, Times, FromChannel, FromMap |
| Intermediate (lazy) | methods | Map, Filter, FlatMap, FilterMap, Reject, Take/Drop, TakeWhile/DropWhile, Scan, Chunk, Window, DistinctBy, Enumerate |
| Terminal | methods | Collect, Fold, Reduce, Find, Any/All/None, GroupBy, KeyBy, Partition, SumBy, MaxByKey, Join |
| Grouping | free functions | PartitionBy (ordered groups as Seq2[K,[]T]) |
| Constrained | free functions | Distinct, Contains, Max/Min, Sum/Product/Mean, Sort, Union/Intersect/Difference, Compact, Without |
| Constrained subtypes | functions (entry) + methods | Comparable/Ordered/Numbers enter; then chain .Distinct(), .Max(), .Sum(), .Replace()/.ReplaceAll(); downgrade with .Ordered()/.Comparable() |
| Multi-sequence | free functions | Zip/Zip3/Zip4, ZipWith, ZipMap, Unzip, Flatten, Concat, Interleave |
Seq2[K,V] |
methods + functions | MapValues, MapKeys, Keys, Values; ToMap, CollectPairs, Associate |
Optional[T] |
type + methods + function | Some/None/ToOptional; Get, Unwrap/UnwrapOr/OrElse, Map, Filter, FlatMap; MapOptional (type-changing) |
The full, authoritative list with one-line semantics for each entry will live in API.md. The design rationale is in tasks/design-seq.md; the complete API inventory is in tasks/prd-seq-api-inventory.md.
Optional[T] is the zero-dependency type used to express "maybe no result". The partial-result terminals — Find, FindLast, First, Last, Nth, Reduce, MaxBy/MinBy/MaxByKey/MinByKey, the package-level Max/Min, and the index-returning FindIndex/IndexOf/LastIndexOf (as Optional[int]) — all return an Optional directly, so you can chain post-processing without unpacking:
out := s.Find(func(x int) bool { return x > 2 }).
Map(func(x int) int { return x * 10 }).
OrElse(-1)Seq2.Find returns Optional[Pair[K, V]] (the key/value wrapped in a Pair). To drop back to Go's idiomatic (value, ok) pair, call .Get():
if v, ok := s.Max().Get(); ok { use(v) }ToOptional(v, ok) bridges the other direction — wrapping a legacy (value, ok) pair into an Optional.
Because Go methods cannot introduce new type parameters, Optional.Map is same-type only; use the package-level MapOptional[T, U] to change the element type (e.g. Optional[int] �� Optional[string]).
Not included (by deliberate decision, see the design doc for the reasoning):
- Error-handling chains (
Seq2[T, error]short-circuiting) — no clean fit on Go's lazy iterators; deferred to a separate proposal. - Parallel execution (
lo/parallel) — this version is sequential and lazy only. - In-place mutation (
lo/mutable) — conflicts with the lazy, immutable model. - Arbitrary-depth flatten (
flattenDeep) — Go's type system can't express it; only one-levelFlatten. - High-arity tuples (
Tuple5–Tuple22) — capped atTuple4; use named structs for more fields. - HKT / type classes and generic methods satisfying interfaces — the language doesn't support these.
Draft. The library requires Go 1.27 (go.mod declares go 1.27); a 1.27 toolchain is needed to build, currently go1.27rc1. Work is split into two batches:
- Batch ① — core types and free functions (
From,Distinct,Max,Zip, …). Independently useful — effectively "alowith the correct constraint split" — and compiles on Go 1.23+ if split out on its own. - Batch ② — the chainable methods (
Map,Filter,Fold, …) that depend on Go 1.27 generic methods.
The generic methods proposal (golang/go#77273) has been accepted and is implemented in Go 1.27. The remaining gap is only that 1.27 is currently an RC, not yet a stable release; pinning the minimum version at 1.27 is the cost we accept for the method chain.
See LICENSE.