Functional data ops: q.Map, q.Filter, q.Fold, q.Reduce, …¶
Functional data manipulation over slices. Pure runtime helpers — no preprocessor rewriting on the value path. The …Err flavours flow naturally through q.Try / q.TryE / q.Ok for the bubble path. Inspiration drawn from Scala collections and samber/lo.
Two flavours per fallible op¶
// Bare — fn cannot fail.
items := q.Map(rows, parseRow)
// …Err — fn returns (R, error). First error short-circuits.
items, err := q.MapErr(rows, parseRowErr)
// Compose with q.Try / q.TryE for the bubble shape:
items := q.Try(q.MapErr(rows, parseRowErr))
items := q.TryE(q.MapErr(rows, parseRowErr)).Wrap("loading users")
There is no …E chain flavour of these helpers. q.TryE(q.MapErr(…)).Wrap(…) already produces the chain shape without a separate API. Don't multiply entry points without earning it.
Catalog (first wave)¶
// Slice transforms
func Map[T, R any](slice []T, fn func(T) R) []R
func MapErr[T, R any](slice []T, fn func(T) (R, error)) ([]R, error)
func FlatMap[T, R any](slice []T, fn func(T) []R) []R
func FlatMapErr[T, R any](slice []T, fn func(T) ([]R, error)) ([]R, error)
func Filter[T any](slice []T, pred func(T) bool) []T
func FilterErr[T any](slice []T, pred func(T) (bool, error)) ([]T, error)
func GroupBy[T any, K comparable](slice []T, fn func(T) K) map[K][]T
// Map ops (operate on map[K]V → map[K]V2)
func MapValues[K comparable, V1, V2 any](m map[K]V1, fn func(V1) V2) map[K]V2
func MapValuesErr[K comparable, V1, V2 any](m map[K]V1, fn func(V1) (V2, error)) (map[K]V2, error)
func MapKeys[K1, K2 comparable, V any](m map[K1]V, fn func(K1) K2) map[K2]V
func MapKeysErr[K1, K2 comparable, V any](m map[K1]V, fn func(K1) (K2, error)) (map[K2]V, error)
func MapEntries[K1, K2 comparable, V1, V2 any](m map[K1]V1, fn func(K1, V1) (K2, V2)) map[K2]V2
func MapEntriesErr[K1, K2 comparable, V1, V2 any](m map[K1]V1, fn func(K1, V1) (K2, V2, error)) (map[K2]V2, error)
func Keys[K comparable, V any](m map[K]V) []K // slices.Collect(maps.Keys(m))
func Values[K comparable, V any](m map[K]V) []V // slices.Collect(maps.Values(m))
// Zip / Unzip
type Pair[A, B any] struct{ First A; Second B }
func Zip[A, B any](as []A, bs []B) []Pair[A, B] // truncates to min(len(as), len(bs))
func Unzip[A, B any](pairs []Pair[A, B]) ([]A, []B)
func ZipMap[K comparable, V any](keys []K, values []V) map[K]V
// Predicate searches (short-circuiting)
func Exists[T any](slice []T, pred func(T) bool) bool // any
func ExistsErr[T any](slice []T, pred func(T) (bool, error)) (bool, error)
func ForAll[T any](slice []T, pred func(T) bool) bool // all (vacuously true on empty)
func ForAllErr[T any](slice []T, pred func(T) (bool, error)) (bool, error)
func Find[T any](slice []T, pred func(T) bool) (T, bool) // first match (comma-ok)
// Reductions
func Fold[T, R any](slice []T, init R, fn func(R, T) R) R // Scala foldLeft
func FoldErr[T, R any](slice []T, init R, fn func(R, T) (R, error)) (R, error)
func Reduce[T any](slice []T, fn func(T, T) T) T // no init; zero on empty
// Set / shape
func Distinct[T comparable](slice []T) []T // first-occurrence preserving
func DistinctBy[T any, K comparable](slice []T, fn func(T) K) []T // dedup by derived key
func Partition[T any](slice []T, pred func(T) bool) ([]T, []T) // (yes, no)
func Chunk[T any](slice []T, n int) [][]T // panics if n <= 0
func Count[T any](slice []T, pred func(T) bool) int // walks all (no short-circuit)
func Take[T any](slice []T, n int) []T // first n
func Drop[T any](slice []T, n int) []T // skip first n
// Sort — returns a new slice; never mutates the input.
func Sort[T cmp.Ordered](slice []T) []T // ascending
func SortBy[T any, K cmp.Ordered](slice []T, fn func(T) K) []T // by derived key
func SortFunc[T any](slice []T, less func(a, b T) int) []T // by comparator
// Aggregations (comma-ok where empty is meaningful)
func Sum[T cmp.Ordered](slice []T) T // additive identity on empty
func Min[T cmp.Ordered](slice []T) (T, bool) // (zero, false) on empty
func Max[T cmp.Ordered](slice []T) (T, bool)
func MinBy[T any, K cmp.Ordered](slice []T, fn func(T) K) (T, bool)
func MaxBy[T any, K cmp.Ordered](slice []T, fn func(T) K) (T, bool)
// Side-effect iteration
func ForEach[T any](slice []T, fn func(T))
func ForEachErr[T any](slice []T, fn func(T) error) error // first error short-circuits
Fold vs Reduce¶
The two are distinct — keep the Scala separation rather than collapsing into one over-loaded Reduce.
q.Fold |
q.Reduce |
|
|---|---|---|
| Init value | explicit | first element (or zero on empty) |
| Accumulator type | may differ from element type | same as element type |
| Empty input | returns init |
returns T's zero value |
| Single element | fn(init, x) runs once |
returns the element unchanged (fn not called) |
// Fold — explicit identity, R can differ from T
sum := q.Fold(nums, 0, func(acc, n int) int { return acc + n })
csv := q.Fold(items, "", func(acc string, it Item) string {
if acc == "" { return it.Name }
return acc + "," + it.Name
})
// Reduce — no init, T-only
total := q.Reduce(nums, func(a, b int) int { return a + b })
joined := q.Reduce(parts, func(a, b string) string { return a + "/" + b })
q.Reduce on empty input¶
q.Reduce returns T's zero value when the slice is empty. This is sound when fn is monoidal — i.e. fn(zero, x) == x:
- ✅ sum (
0 + x == x) - ✅ string concat (
"" + x == x) - ✅ slice append (
nil append x == x)
It is silently wrong for non-monoidal fn:
- ❌ max —
max(0, -5)is0, but the empty result is meaningless - ❌ min — same in reverse
- ❌ multiply —
0 * xis0, identity should be1 - ❌ struct types — zero
T{}rarely satisfiesfn(zero, x) == x
For the second category, reach for q.Fold with an explicit identity:
Or, if your fn really has no natural identity, distinguish empty up front:
Map ops: MapValues and MapKeys¶
These operate on map[K]V, not []T — they're the natural complement to slice transforms when you're already working with maps (often the output of q.GroupBy).
// Group then aggregate — the most common shape
counts := q.MapValues(q.GroupBy(items, byCat),
func(g []Item) int { return len(g) })
sums := q.MapValues(q.GroupBy(orders, byCustomer),
func(g []Order) int { return q.Fold(g, 0, addAmount) })
// Rename keys
upper := q.MapKeys(byCat, strings.ToUpper)
Caveats:
- Iteration order is map-random.
MapValuesErr/MapKeysErrshort-circuit on the first error, but "first" is whichever the runtime visited first — not input-defined. MapKeyscollisions are last-write-wins. If two source keys map to the same target key, only one value survives, and which one is undefined.
q.MapEntries for the combined transform¶
When both keys and values change in a way that depends on each other, q.MapEntries(m, func(K1, V1) (K2, V2)) map[K2]V2 does it in a single pass. Otherwise you'd chain MapValues then MapKeys (two passes) or write the loop by hand:
canonical := q.MapEntries(byID, func(id int, a alias) (string, int) {
return strings.ToLower(a.name), a.v
})
q.Keys / q.Values¶
Thin wrappers over slices.Collect(maps.Keys(m)) / slices.Collect(maps.Values(m)) — the stdlib already provides this since Go 1.23, q just saves the import + the two-step incantation. Order is unspecified.
Why no q.ToMap / q.Associate?¶
Building a map from a slice via a func(T) (K, V) projection is a one-liner — for _, x := range xs { k, v := fn(x); m[k] = v } — and silently drops collisions either to first or last value, depending on which side of the loop wins. q.GroupBy + q.MapValues makes the keep-first / keep-last / aggregate decision explicit.
Pipelining¶
The bare ops chain naturally because each returns a slice (or compatible). Read inside-out the way Go forces — there is no method-chain syntax:
total := q.Fold(
q.Filter(
q.Map(items, scoreOf),
func(s int) bool { return s > 50 },
),
0,
func(acc, s int) int { return acc + s },
)
A chain of three nested calls is the upper end of comfortable; past that, name the intermediates:
scores := q.Map(items, scoreOf)
high := q.Filter(scores, func(s int) bool { return s > 50 })
total := q.Fold(high, 0, func(acc, s int) int { return acc + s })
The ops compile to plain for loops with no per-element heap allocation beyond the output slice — same code you'd write by hand.
Iterator (iter.Seq) variants — deferred¶
Go 1.23 ships iter.Seq / iter.Seq2. q's first wave is slice-only. Iterator-input variants (q.MapSeq, q.FilterSeq, …) are a follow-up wave once usage patterns settle. Slice → iterator can be done by hand via slices.Values; the reverse via slices.Collect. q won't paper over the conversion until there's a clear ergonomic win.
Why no …E chain flavour?¶
q.TryE(q.MapErr(…)).Wrap(…) already produces the chain shape via existing rewriter machinery. A separate q.MapE would duplicate that without adding capability.
See also¶
q.Try/q.TryE— bubble + chain over(T, error). Pairs with…Errvariants.q.Ok/q.OkE— bubble + chain over(T, bool). Pairs withq.Find.q.AwaitAll— concurrent[]Future[T] → []T. Different concern (parallelism over completed values vs. functional ops over a slice).q.ParMap(TODO #81) — parallel variants of these ops, defaultruntime.NumCPU(). Not yet shipped.