Skip to content

A fluent, Laravel-inspired Collection library for Go - with chaining, higher-order functions, and expressive data manipulation.

License

Notifications You must be signed in to change notification settings

goforj/collection

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

goforj/collection logo

Fluent, Laravel-style Collections for Go - with generics, chainable pipelines, and expressive data transforms.

Go Reference License: MIT Go Test Go version Latest tag Tests Go Report Card

collection brings an expressive, fluent API to Go. Iterate, filter, transform, sort, reduce, group, and debug your data with zero dependencies. Designed to feel natural to Go developers - and luxurious to everyone else.

Features

  • Fluent chaining - pipeline your operations like Laravel Collections
  • Fully generic (Collection[T]) - no reflection, no interface{}
  • Zero dependencies - pure Go, fast, lightweight
  • Minimal allocations - avoids unnecessary copies; most operations reuse the underlying slice
  • Map / Filter / Reduce - clean functional transforms
  • First / Last / Find / Contains helpers
  • Sort, GroupBy, Chunk, and more
  • Safe-by-default - defensive copies where appropriate
  • Built-in JSON helpers (ToJSON(), ToPrettyJSON())
  • Developer-friendly debug helpers (Dump(), Dd(), DumpStr())
  • Works with any Go type, including structs, pointers, and deeply nested composites

Fluent Chaining

Many methods return the collection itself, allowing for fluent method chaining.

Some methods maybe limited to due to go's generic constraints.

Fluent example:
examples/chaining/main.go

events := []DeviceEvent{
    {Device: "router-1", Region: "us-east", Errors: 3},
    {Device: "router-2", Region: "us-east", Errors: 15},
    {Device: "router-3", Region: "us-west", Errors: 22},
}

// Fluent slice pipeline
collection.
    New(events). // Construction
    Shuffle(). // Ordering
    Filter(func(e DeviceEvent) bool { return e.Errors > 5 }). // Slicing
    Sort(func(a, b DeviceEvent) bool { return a.Errors > b.Errors }). // Ordering
    Take(5). // Slicing
    TakeUntilFn(func(e DeviceEvent) bool { return e.Errors < 10 }). // Slicing (stop when predicate becomes true)
    SkipLast(1). // Slicing
    Dump() // Debugging

// []main.DeviceEvent [
//  0 => #main.DeviceEvent {
//    +Device => "router-3" #string
//    +Region => "us-west" #string
//    +Errors => 22 #int
//  }
// ]

Performance Benchmarks

lo is a fantastic library and a major inspiration for this project. Our focus differs: collection is built for fluent chaining with explicit mutability, which lets hot paths avoid intermediate allocations. That shows up most in chained pipelines and in-place operations where we can keep work on the same backing slice while still being explicit about behavior.

Op ns/op (vs lo) × bytes/op (vs lo) × allocs/op (vs lo)
All 232ns / 230ns 0B / 0B 0 / 0
Any 232ns / 234ns 0B / 0B 0 / 0
Chunk 128ns / 1.1µs 8.30x 1.3KB / 9.3KB 7.25x less 1 / 51
Contains 238ns / 250ns 1.05x 0B / 0B 0 / 0
CountBy 8.1µs / 8.2µs 9.4KB / 9.4KB 11 / 11
CountByValue 8.1µs / 8.1µs 9.4KB / 9.4KB 11 / 11
Difference 19.4µs / 44.5µs 2.29x 82.1KB / 108.8KB 1.33x less 12 / 43
Each 235ns / 230ns 0B / 0B 0 / 0
Filter 647ns / 1.1µs 1.67x 0B / 8.2KB ∞x less 0 / 1
Find 239ns / 235ns 0B / 0B 0 / 0
First 0ns / 0ns 0B / 0B 0 / 0
GroupBySlice 8.2µs / 8.3µs 21.0KB / 21.0KB 83 / 83
IndexWhere 232ns / 231ns 0B / 0B 0 / 0
Intersect 11.0µs / 10.8µs 11.4KB / 11.4KB 19 / 19
Last 0ns / 0ns 0B / 0B 0 / 0
Map 347ns / 821ns 2.37x 0B / 8.2KB ∞x less 0 / 1
Max 230ns / 231ns 0B / 0B 0 / 0
Min 232ns / 229ns 0B / 0B 0 / 0
None 232ns / 232ns 0B / 0B 0 / 0
Pipeline F→M→T→R 496ns / 1.3µs 2.62x 0B / 12.3KB ∞x less 0 / 2
Reduce (sum) 230ns / 231ns 0B / 0B 0 / 0
Reverse 216ns / 230ns 1.06x 0B / 0B 0 / 0
Shuffle 3.6µs / 5.3µs 1.49x 0B / 0B 0 / 0
Skip 0ns / 721ns 0B / 8.2KB ∞x less 0 / 1
SkipLast 0ns / 730ns 0B / 8.2KB ∞x less 0 / 1
Sum 232ns / 233ns 0B / 0B 0 / 0
Take 0ns / 0ns 0B / 0B 0 / 0
ToMap 7.7µs / 7.8µs 36.9KB / 37.0KB 5 / 6
Union 17.4µs / 17.7µs 90.3KB / 90.3KB 11 / 10
Unique 6.4µs / 6.5µs 45.1KB / 45.1KB 6 / 6
UniqueBy 6.9µs / 6.7µs 45.2KB / 45.1KB 7 / 6
Zip 1.4µs / 3.2µs 2.27x 16.4KB / 16.4KB 1 / 1
ZipWith 1.0µs / 3.1µs 3.07x 8.2KB / 8.2KB 1 / 1

How to read the benchmarks

  • means the two libraries are effectively equivalent
  • ∞x less means one side allocates while the other allocates nothing
  • Single-operation helpers are intentionally close in performance if not exceeds
  • Multi-step pipelines highlight the architectural difference

If you prefer immutable, one-off helpers - lo is outstanding. If you write expressive, chained data pipelines and care about hot-path performance - collection is built for that job.

Performance Philosophy

tl;dr: lo is excellent. We solve a different problem - and in chained pipelines, that difference matters.

lo is a fantastic library and a major inspiration for this project. It is battle-tested, idiomatic, and often the right choice when you want small, standalone helpers that operate on slices in isolation.

collection takes a different approach.

Rather than treating each operation as an independent transformation, collection is built around explicit, fluent pipelines. Many operations are designed to mutate the same backing slice intentionally, allowing chained workflows to avoid intermediate allocations and unnecessary copying - while still making that behavior visible and documented.

That design choice doesn't matter much for some single operations. It matters a lot once you start chaining and especially in hot paths.

Why chaining changes the performance story

Most functional helpers (including lo) operate like this:

input → Map → new slice → Filter → new slice → Take → new slice

That model is simple and safe - but each step typically allocates.

collection pipelines are designed to look more like this:

input → Filter (in place) → Sort (in place) → Take (slice view)

When you opt into mutation, the pipeline stays on the same backing array unless an operation explicitly documents that it allocates. The result is:

  • Fewer allocations
  • Less GC pressure
  • Lower end-to-end latency in hot paths
  • Much stronger scaling in multi-step pipelines

That's why the biggest deltas appear in benchmarks like:

  • Pipeline F→M→T→R
  • Map
  • Filter
  • Chunk
  • Zip / ZipWith
  • Skip / SkipLast

In these cases, collection can be 2×–30× faster and often reduce allocations to zero, not by doing "clever tricks", but by making mutation explicit and opt-in.

Explicit branching with Clone

Fluent pipelines don't mean you're locked into mutation.

When you want to branch a pipeline or preserve the original data, Clone() creates a shallow copy of the collection so subsequent operations are isolated and predictable.

events := collection.New(deviceEvents)

// Fast alerting path: cheap filters, early exit
alerts := events.
    Clone().
    Filter(func(e DeviceEvent) bool { return e.Severity >= Critical }).
    Take(10)

// Deeper analysis path: heavier work, full ordering
report := events.
    Clone().
    Filter(func(e DeviceEvent) bool { return e.Region == "us-east" }).
    Sort(func(a, b DeviceEvent) bool { return a.Timestamp.Before(b.Timestamp) })

This keeps the performance benefits of in-place operations where they matter, while making divergence points explicit and intentional.

No hidden copies. No surprises.

Design Principles

  • Type-safe: no reflection, no any leaks
  • Explicit semantics: order, mutation, and allocation are documented
  • Go-native: respects generics and stdlib patterns
  • Eager evaluation: no lazy pipelines or hidden concurrency
  • Maps are boundaries: unordered data is handled explicitly

What this library is not

  • Not a lazy or streaming library
  • Not concurrency-aware
  • Not immutable-by-default
  • Not a replacement for idiomatic loops in simple cases

Working with maps

Maps are unordered in Go. This library does not pretend otherwise.

Instead, map interaction is explicit and intentional:

  • FromMap materializes key/value pairs into an ordered workflow
  • ToMap reduces collections back into maps explicitly
  • ToMapKV provides a convenience for Pair[K,V]

This makes transitions between unordered and ordered data visible and honest.

Behavior semantics

Each method declares how it interacts with the collection:

  • readonly – reads data only, returns a derived value
  • immutable – returns a new collection, original unchanged
  • mutable – modifies the collection in place

Annotations describe observable behavior, not implementation details.

Runnable examples

Every function has a corresponding runnable example under ./examples.

These examples are generated directly from the documentation blocks of each function, ensuring the docs and code never drift. These are the same examples you see here in the README and GoDoc.

An automated test executes every example to verify it builds and runs successfully.

This guarantees all examples are valid, up-to-date, and remain functional as the API evolves.

Installation

go get github.com/goforj/collection

API Index

Group Functions
Access Items
Aggregation Avg Count CountBy CountByValue Max MaxBy Median Min MinBy Mode Reduce Sum
Construction Clone New NewNumeric
Debugging Dd Dump DumpStr
Grouping GroupBy GroupBySlice
Maps FromMap ToMap ToMapKV
Ordering After Before Reverse Shuffle Sort
Querying All Any At Contains FindWhere First FirstWhere IndexWhere IsEmpty Last LastWhere None
Serialization ToJSON ToPrettyJSON
Set Operations Difference Intersect SymmetricDifference Union Unique UniqueBy UniqueComparable
Slicing Chunk Filter Partition Pop PopN Skip SkipLast Take TakeLast TakeUntil TakeUntilFn Window
Transformation Append Concat Each Map MapTo Merge Multiply Pipe Pluck Prepend Push Tap Times Transform Zip ZipWith

Access

Items · readonly · fluent

Items returns the underlying slice of items.

Example: integers

c := collection.New([]int{1, 2, 3})
items := c.Items()
collection.Dump(items)
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
// ]

Example: strings

c2 := collection.New([]string{"apple", "banana"})
items2 := c2.Items()
collection.Dump(items2)
// #[]string [
//   0 => "apple" #string
//   1 => "banana" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

out := users.Items()
collection.Dump(out)
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
//   1 => #main.User {
//     +ID   => 2 #int
//     +Name => "Bob" #string
//   }
// ]

Aggregation

Avg · readonly

Avg returns the average of the collection values as a float64. If the collection is empty, Avg returns 0.

Example: integers

c := collection.NewNumeric([]int{2, 4, 6})
collection.Dump(c.Avg())
// 4.000000 #float64

Example: float

c2 := collection.NewNumeric([]float64{1.5, 2.5, 3.0})
collection.Dump(c2.Avg())
// 2.333333 #float64

Count · readonly · fluent

Count returns the total number of items in the collection.

count := collection.New([]int{1, 2, 3, 4}).Count()
collection.Dump(count)
// 4 #int

CountBy · readonly

CountBy returns a map of keys extracted by fn to their occurrence counts. K must be comparable.

Example: integers

c := collection.New([]int{1, 2, 2, 3, 3, 3})
counts := collection.CountBy(c, func(v int) int {
	return v
})
collection.Dump(counts)
// map[int]int {
//   1: 1 #int
//   2: 2 #int
//   3: 3 #int
// }

Example: strings

c2 := collection.New([]string{"apple", "banana", "apple", "cherry", "banana"})
counts2 := collection.CountBy(c2, func(v string) string {
	return v
})
collection.Dump(counts2)
// map[string]int {
//   "apple":  2 #int
//   "banana": 2 #int
//   "cherry": 1 #int
// }

Example: structs

type User struct {
	Name string
	Role string
}

users := collection.New([]User{
	{Name: "Alice", Role: "admin"},
	{Name: "Bob", Role: "user"},
	{Name: "Carol", Role: "admin"},
	{Name: "Dave", Role: "user"},
	{Name: "Eve", Role: "admin"},
})

roleCounts := collection.CountBy(users, func(u User) string {
	return u.Role
})

collection.Dump(roleCounts)
// map[string]int {
//   "admin": 3 #int
//   "user":  2 #int
// }

CountByValue · readonly

CountByValue returns a map where each distinct item in the collection is mapped to the number of times it appears.

Example: strings

c1 := collection.New([]string{"a", "b", "a"})
counts1 := collection.CountByValue(c1)
collection.Dump(counts1)
// #map[string]int [
//	"a" => 2 #int
//	"b" => 1 #int
// ]

Example: integers

c2 := collection.New([]int{1, 2, 2, 3, 3, 3})
counts2 := collection.CountByValue(c2)
collection.Dump(counts2)
// #map[int]int [
//	1 => 1 #int
//	2 => 2 #int
//	3 => 3 #int
// ]

Example: structs (comparable)

type Point struct {
	X int
	Y int
}

c3 := collection.New([]Point{
	{X: 1, Y: 1},
	{X: 2, Y: 2},
	{X: 1, Y: 1},
})

counts3 := collection.CountByValue(c3)
collection.Dump(counts3)
// #map[collection.Point]int [
//	{X:1 Y:1} => 2 #int
//	{X:2 Y:2} => 1 #int
// ]

Max · readonly

Max returns the largest numeric item in the collection. The second return value is false if the collection is empty.

Example: integers

c := collection.NewNumeric([]int{3, 1, 2})

max1, ok1 := c.Max()
collection.Dump(max1, ok1)
// 3    #int
// true #bool

Example: floats

c2 := collection.NewNumeric([]float64{1.5, 9.2, 4.4})

max2, ok2 := c2.Max()
collection.Dump(max2, ok2)
// 9.200000 #float64
// true     #bool

Example: empty numeric collection

c3 := collection.NewNumeric([]int{})

max3, ok3 := c3.Max()
collection.Dump(max3, ok3)
// 0     #int
// false #bool

MaxBy · readonly

MaxBy returns the item whose key (produced by keyFn) is the largest. The second return value is false if the collection is empty.

Example: structs - highest score

type Player struct {
	Name  string
	Score int
}

players := collection.New([]Player{
	{Name: "Alice", Score: 10},
	{Name: "Bob", Score: 25},
	{Name: "Carol", Score: 18},
})

top, ok := collection.MaxBy(players, func(p Player) int {
	return p.Score
})

collection.Dump(top, ok)
// #main.Player {
//   +Name  => "Bob" #string
//   +Score => 25 #int
// }
// true #bool

Example: strings - longest length

words := collection.New([]string{"go", "collection", "rocks"})

longest, ok := collection.MaxBy(words, func(s string) int {
	return len(s)
})

collection.Dump(longest, ok)
// "collection" #string
// true #bool

Example: empty collection

empty := collection.New([]int{})
maxVal, ok := collection.MaxBy(empty, func(v int) int { return v })
collection.Dump(maxVal, ok)
// 0 #int
// false #bool

Median · readonly

Median returns the statistical median of the numeric collection as float64. Returns (0, false) if the collection is empty.

Example: integers - odd number of items

c := collection.NewNumeric([]int{3, 1, 2})

median1, ok1 := c.Median()
collection.Dump(median1, ok1)
// 2.000000 #float64
// true     #bool

Example: integers - even number of items

c2 := collection.NewNumeric([]int{10, 2, 4, 6})

median2, ok2 := c2.Median()
collection.Dump(median2, ok2)
// 5.000000 #float64
// true     #bool

Example: floats

c3 := collection.NewNumeric([]float64{1.1, 9.9, 3.3})

median3, ok3 := c3.Median()
collection.Dump(median3, ok3)
// 3.300000 #float64
// true     #bool

Example: integers - empty numeric collection

c4 := collection.NewNumeric([]int{})

median4, ok4 := c4.Median()
collection.Dump(median4, ok4)
// 0.000000 #float64
// false    #bool

Min · readonly

Min returns the smallest numeric item in the collection. The second return value is false if the collection is empty.

Example: integers

c := collection.NewNumeric([]int{3, 1, 2})
min, ok := c.Min()
collection.Dump(min, ok)
// 1 #int
// true #bool

Example: floats

c2 := collection.NewNumeric([]float64{2.5, 9.1, 1.2})
min2, ok2 := c2.Min()
collection.Dump(min2, ok2)
// 1.200000 #float64
// true #bool

Example: integers - empty collection

empty := collection.NewNumeric([]int{})
min3, ok3 := empty.Min()
collection.Dump(min3, ok3)
// 0 #int
// false #bool

MinBy · readonly

MinBy returns the item whose key (produced by keyFn) is the smallest. The second return value is false if the collection is empty.

Example: structs - smallest age

type User struct {
	Name string
	Age  int
}

users := collection.New([]User{
	{Name: "Alice", Age: 30},
	{Name: "Bob", Age: 25},
	{Name: "Carol", Age: 40},
})

minUser, ok := collection.MinBy(users, func(u User) int {
	return u.Age
})

collection.Dump(minUser, ok)
// #main.User {
//   +Name => "Bob" #string
//   +Age  => 25 #int
// }
// true #bool

Example: strings - shortest length

words := collection.New([]string{"apple", "fig", "banana"})

shortest, ok := collection.MinBy(words, func(s string) int {
	return len(s)
})

collection.Dump(shortest, ok)
// "fig" #string
// true #bool

Example: empty collection

empty := collection.New([]int{})
minVal, ok := collection.MinBy(empty, func(v int) int { return v })
collection.Dump(minVal, ok)
// 0 #int
// false #bool

Mode · readonly

Mode returns the most frequent numeric value(s) in the collection. If multiple values tie for highest frequency, all are returned in first-seen order.

Example: integers – single mode

c := collection.NewNumeric([]int{1, 2, 2, 3})
mode := c.Mode()
collection.Dump(mode)
// #[]int [
//   0 => 2 #int
// ]

Example: integers – tie for mode

c2 := collection.NewNumeric([]int{1, 2, 1, 2})
mode2 := c2.Mode()
collection.Dump(mode2)
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
// ]

Example: floats

c3 := collection.NewNumeric([]float64{1.1, 2.2, 1.1, 3.3})
mode3 := c3.Mode()
collection.Dump(mode3)
// #[]float64 [
//   0 => 1.100000 #float64
// ]

Example: integers - empty collection

empty := collection.NewNumeric([]int{})
mode4 := empty.Mode()
collection.Dump(mode4)
// <nil>

Reduce · readonly · fluent

Reduce collapses the collection into a single accumulated value. The accumulator has the same type T as the collection's elements.

Example: integers - sum

sum := collection.New([]int{1, 2, 3}).Reduce(0, func(acc, n int) int {
	return acc + n
})
collection.Dump(sum)
// 6 #int

Example: strings

joined := collection.New([]string{"a", "b", "c"}).Reduce("", func(acc, s string) string {
	return acc + s
})
collection.Dump(joined)
// "abc" #string

Example: structs

type Stats struct {
	Count int
	Sum   int
}

stats := collection.New([]Stats{
	{Count: 1, Sum: 10},
	{Count: 1, Sum: 20},
	{Count: 1, Sum: 30},
})

total := stats.Reduce(Stats{}, func(acc, s Stats) Stats {
	acc.Count += s.Count
	acc.Sum += s.Sum
	return acc
})

collection.Dump(total)
// #main.Stats [
//   +Count => 3 #int
//   +Sum   => 60 #int
// ]

Sum · readonly

Sum returns the sum of all numeric items in the NumericCollection. If the collection is empty, Sum returns the zero value of T.

Example: integers

c := collection.NewNumeric([]int{1, 2, 3})
total := c.Sum()
collection.Dump(total)
// 6 #int

Example: floats

c2 := collection.NewNumeric([]float64{1.5, 2.5})
total2 := c2.Sum()
collection.Dump(total2)
// 4.000000 #float64

Example: integers - empty collection

c3 := collection.NewNumeric([]int{})
total3 := c3.Sum()
collection.Dump(total3)
// 0 #int

Construction

Clone · allocates · fluent

Clone returns a shallow copy of the collection.

Example: basic cloning

c := collection.New([]int{1, 2, 3})
clone := c.Clone()

clone.Push(4)

collection.Dump(c.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
// ]

collection.Dump(clone.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
//   3 => 4 #int
// ]

Example: branching pipelines

base := collection.New([]int{1, 2, 3, 4, 5})

evens := base.Clone().Filter(func(v int) bool {
	return v%2 == 0
})

odds := base.Clone().Filter(func(v int) bool {
	return v%2 != 0
})

collection.Dump(base.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
//   3 => 4 #int
//   4 => 5 #int
// ]

collection.Dump(evens.Items())
// #[]int [
//   0 => 2 #int
//   1 => 4 #int
// ]

collection.Dump(odds.Items())
// #[]int [
//   0 => 1 #int
//   1 => 3 #int
//   2 => 5 #int
// ]

New · immutable · fluent

New creates a new Collection from the provided slice.

NewNumeric · immutable · fluent

NewNumeric wraps a slice of numeric types in a NumericCollection. A shallow copy is made so that further operations don't mutate the original slice.

Debugging

Dd · fluent

Dd prints items then terminates execution. Like Laravel's dd(), this is intended for debugging and should not be used in production control flow.

c := collection.New([]string{"a", "b"})
c.Dd()
// #[]string [
//   0 => "a" #string
//   1 => "b" #string
// ]
// Process finished with the exit code 1

Dump · readonly · fluent

Dump prints items with godump and returns the same collection. This is a no-op on the collection itself and never panics.

Example: integers

c := collection.New([]int{1, 2, 3})
c.Dump()
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
// ]

Example: integers - chaining

collection.New([]int{1, 2, 3}).
	Filter(func(v int) bool { return v > 1 }).
	Dump()
// #[]int [
//   0 => 2 #int
//   1 => 3 #int
// ]

Example: integers

c2 := collection.New([]int{1, 2, 3})
collection.Dump(c2.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
// ]

DumpStr · readonly · fluent

DumpStr returns the pretty-printed dump of the items as a string, without printing or exiting. Useful for logging, snapshot testing, and non-interactive debugging.

c := collection.New([]int{10, 20})
s := c.DumpStr()
fmt.Println(s)
// #[]int [
//   0 => 10 #int
//   1 => 20 #int
// ]

Grouping

GroupBy · readonly

GroupBy partitions the collection into groups keyed by the value returned from keyFn.

Example: grouping integers by parity

values := []int{1, 2, 3, 4, 5}

groups := collection.GroupBy(
	collection.New(values),
	func(v int) string {
		if v%2 == 0 {
			return "even"
		}
		return "odd"
	},
)

collection.Dump(groups["even"].Items())
// []int [
//  0 => 2 #int
//  1 => 4 #int
// ]
collection.Dump(groups["odd"].Items())
// []int [
//  0 => 1 #int
//  1 => 3 #int
//  2 => 5 #int
// ]

Example: grouping structs by field

type User struct {
	ID   int
	Role string
}

users := []User{
	{ID: 1, Role: "admin"},
	{ID: 2, Role: "user"},
	{ID: 3, Role: "admin"},
}

groups2 := collection.GroupBy(
	collection.New(users),
	func(u User) string { return u.Role },
)

collection.Dump(groups2["admin"].Items())
// []main.User [
//  0 => #main.User {
//    +ID   => 1 #int
//    +Role => "admin" #string
//  }
//  1 => #main.User {
//    +ID   => 3 #int
//    +Role => "admin" #string
//  }
// ]
collection.Dump(groups2["user"].Items())
// []main.User [
//  0 => #main.User {
//    +ID   => 2 #int
//    +Role => "user" #string
//  }
// ]

GroupBySlice · readonly

GroupBySlice partitions the collection into groups keyed by the value returned from keyFn.

Example: grouping integers by parity

values := []int{1, 2, 3, 4, 5}

groups := collection.GroupBySlice(
	collection.New(values),
	func(v int) string {
		if v%2 == 0 {
			return "even"
		}
		return "odd"
	},
)

collection.Dump(groups["even"])
// []int [
//  0 => 2 #int
//  1 => 4 #int
// ]
collection.Dump(groups["odd"])
// []int [
//  0 => 1 #int
//  1 => 3 #int
//  2 => 5 #int
// ]

Example: grouping structs by field

type User struct {
	ID   int
	Role string
}

users := []User{
	{ID: 1, Role: "admin"},
	{ID: 2, Role: "user"},
	{ID: 3, Role: "admin"},
}

groups2 := collection.GroupBySlice(
	collection.New(users),
	func(u User) string { return u.Role },
)

collection.Dump(groups2["admin"])
// []main.User [
//  0 => #main.User {
//    +ID   => 1 #int
//    +Role => "admin" #string
//  }
//  1 => #main.User {
//    +ID   => 3 #int
//    +Role => "admin" #string
//  }
// ]
collection.Dump(groups2["user"])
// []main.User [
//  0 => #main.User {
//    +ID   => 2 #int
//    +Role => "user" #string
//  }
// ]

Maps

FromMap · immutable · fluent

FromMap materializes a map into a collection of key/value pairs.

Example: basic usage

m := map[string]int{
	"a": 1,
	"b": 2,
	"c": 3,
}

c := collection.FromMap(m)
collection.Dump(c.Items())

// #[]collection.Pair[string,int] [
//   0 => {Key:"a" Value:1}
//   1 => {Key:"b" Value:2}
//   2 => {Key:"c" Value:3}
// ]

Example: filtering map entries

type Config struct {
	Enabled bool
	Timeout int
}

configs := map[string]Config{
	"router-1": {Enabled: true,  Timeout: 30},
	"router-2": {Enabled: false, Timeout: 10},
	"router-3": {Enabled: true,  Timeout: 45},
}

out := collection.
	FromMap(configs).
	Filter(func(p collection.Pair[string, Config]) bool {
		return p.Value.Enabled
	}).
	Items()

collection.Dump(out)

// #[]collection.Pair[string,collection.Config] [
//   0 => {Key:"router-1" Value:{Enabled:true Timeout:30}}
//   1 => {Key:"router-3" Value:{Enabled:true Timeout:45}}
// ]

Example: map → collection → map

users := map[string]int{
	"alice": 1,
	"bob":   2,
}

c2 := collection.FromMap(users)
out2 := collection.ToMapKV(c2)

collection.Dump(out2)

// #map[string]int [
//   "alice" => 1
//   "bob"   => 2
// ]

ToMap · readonly

ToMap reduces a collection into a map using the provided key and value selector functions.

Example: basic usage

users := []string{"alice", "bob", "carol"}

out := collection.ToMap(
	collection.New(users),
	func(name string) string { return name },
	func(name string) int { return len(name) },
)

collection.Dump(out)

Example: re-keying structs

type User struct {
	ID   int
	Name string
}

users2 := []User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
}

byID := collection.ToMap(
	collection.New(users2),
	func(u User) int { return u.ID },
	func(u User) User { return u },
)

collection.Dump(byID)

ToMapKV · readonly

ToMapKV converts a collection of key/value pairs into a map.

Example: basic usage

m := map[string]int{
	"a": 1,
	"b": 2,
	"c": 3,
}

c := collection.FromMap(m)
out := collection.ToMapKV(c)

collection.Dump(out)

// #map[string]int [
//   "a" => 1
//   "b" => 2
//   "c" => 3
// ]

Example: filtering before conversion

type Config struct {
	Enabled bool
	Timeout int
}

configs := map[string]Config{
	"router-1": {Enabled: true,  Timeout: 30},
	"router-2": {Enabled: false, Timeout: 10},
	"router-3": {Enabled: true,  Timeout: 45},
}

c2 := collection.
	FromMap(configs).
	Filter(func(p collection.Pair[string, Config]) bool {
		return p.Value.Enabled
	})

out2 := collection.ToMapKV(c2)

collection.Dump(out2)

// #map[string]collection.Config [
//   "router-1" => {Enabled:true Timeout:30}
//   "router-3" => {Enabled:true Timeout:45}
// ]

Ordering

After · immutable · fluent

After returns all items after the first element for which pred returns true. If no element matches, an empty collection is returned.

c := collection.New([]int{1, 2, 3, 4, 5})
c.After(func(v int) bool { return v == 3 }).Dump()
// #[]int [
//  0 => 4 #int
//  1 => 5 #int
// ]

Before · immutable · fluent

Before returns a new collection containing all items that appear before the first element for which pred returns true.

Example: integers

c1 := collection.New([]int{1, 2, 3, 4, 5})
out1 := c1.Before(func(v int) bool { return v >= 3 })
collection.Dump(out1.Items())
// #[]int [
//	0 => 1 #int
//	1 => 2 #int
// ]

Example: predicate never matches → whole collection returned

c2 := collection.New([]int{10, 20, 30})
out2 := c2.Before(func(v int) bool { return v == 99 })
collection.Dump(out2.Items())
// #[]int [
//	0 => 10 #int
//	1 => 20 #int
//	2 => 30 #int
// ]

Example: structs: get all users before the first admin

type User struct {
	Name  string
	Admin bool
}

c3 := collection.New([]User{
	{Name: "Alice", Admin: false},
	{Name: "Bob", Admin: false},
	{Name: "Eve", Admin: true},
	{Name: "Mallory", Admin: false},
})

out3 := c3.Before(func(u User) bool { return u.Admin })
collection.Dump(out3.Items())
// #[]collection.User [
//	0 => {Name:"Alice" Admin:false}  #collection.User
//	1 => {Name:"Bob"   Admin:false}  #collection.User
// ]

Reverse · mutable · fluent

Reverse reverses the order of items in the collection in place and returns the same collection for chaining.

Example: integers

c := collection.New([]int{1, 2, 3, 4})
c.Reverse()
collection.Dump(c.Items())
// #[]int [
//   0 => 4 #int
//   1 => 3 #int
//   2 => 2 #int
//   3 => 1 #int
// ]

Example: strings – chaining

out := collection.New([]string{"a", "b", "c"}).
	Reverse().
	Append("d").
	Items()

collection.Dump(out)
// #[]string [
//   0 => "c" #string
//   1 => "b" #string
//   2 => "a" #string
//   3 => "d" #string
// ]

Example: structs

type User struct {
	ID int
}

users := collection.New([]User{
	{ID: 1},
	{ID: 2},
	{ID: 3},
})

users.Reverse()
collection.Dump(users.Items())
// #[]collection.User [
//   0 => {ID:3} #collection.User
//   1 => {ID:2} #collection.User
//   2 => {ID:1} #collection.User
// ]

Shuffle · mutable · fluent

Shuffle randomly shuffles the items in the collection in place and returns the same collection for chaining.

Example: integers

c := collection.New([]int{1, 2, 3, 4, 5})
c.Shuffle()
collection.Dump(c.Items())

Example: strings – chaining

out := collection.New([]string{"a", "b", "c"}).
	Shuffle().
	Append("d").
	Items()

collection.Dump(out)

Example: structs

type User struct {
	ID int
}

users := collection.New([]User{
	{ID: 1},
	{ID: 2},
	{ID: 3},
	{ID: 4},
})

users.Shuffle()
collection.Dump(users.Items())

Sort · mutable · fluent

Sort sorts the collection in place using the provided comparison function and returns the same collection for chaining.

Example: integers

c := collection.New([]int{5, 1, 4, 2})
c.Sort(func(a, b int) bool { return a < b })
collection.Dump(c.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 4 #int
//   3 => 5 #int
// ]

Example: strings (descending)

c2 := collection.New([]string{"apple", "banana", "cherry"})
c2.Sort(func(a, b string) bool { return a > b })
collection.Dump(c2.Items())
// #[]string [
//   0 => "cherry" #string
//   1 => "banana" #string
//   2 => "apple" #string
// ]

Example: structs

type User struct {
	Name string
	Age  int
}

users := collection.New([]User{
	{Name: "Alice", Age: 30},
	{Name: "Bob", Age: 25},
	{Name: "Carol", Age: 40},
})

// Sort by age ascending
users.Sort(func(a, b User) bool {
	return a.Age < b.Age
})
collection.Dump(users.Items())
// #[]main.User [
//   0 => #main.User {
//     +Name => "Bob" #string
//     +Age  => 25 #int
//   }
//   1 => #main.User {
//     +Name => "Alice" #string
//     +Age  => 30 #int
//   }
//   2 => #main.User {
//     +Name => "Carol" #string
//     +Age  => 40 #int
//   }
// ]

Querying

All · readonly · fluent

All returns true if fn returns true for every item in the collection. If the collection is empty, All returns true (vacuously true).

Example: integers – all even

c := collection.New([]int{2, 4, 6})
allEven := c.All(func(v int) bool { return v%2 == 0 })
collection.Dump(allEven)
// true #bool

Example: integers – not all even

c2 := collection.New([]int{2, 3, 4})
allEven2 := c2.All(func(v int) bool { return v%2 == 0 })
collection.Dump(allEven2)
// false #bool

Example: strings – all non-empty

c3 := collection.New([]string{"a", "b", "c"})
allNonEmpty := c3.All(func(s string) bool { return s != "" })
collection.Dump(allNonEmpty)
// true #bool

Example: empty collection (vacuously true)

empty := collection.New([]int{})
all := empty.All(func(v int) bool { return v > 0 })
collection.Dump(all)
// true #bool

Any · readonly · fluent

Any returns true if at least one item satisfies fn.

c := collection.New([]int{1, 2, 3, 4})
has := c.Any(func(v int) bool { return v%2 == 0 }) // true
collection.Dump(has)
// true #bool

At · readonly · fluent

At returns the item at the given index and a boolean indicating whether the index was within bounds.

Example: integers

c := collection.New([]int{10, 20, 30})
v, ok := c.At(1)
collection.Dump(v, ok)
// 20 true

Example: out of bounds

v2, ok2 := c.At(10)
collection.Dump(v2, ok2)
// 0 false

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

u, ok3 := users.At(0)
collection.Dump(u, ok3)
// {ID:1 Name:"Alice"} true

Contains · readonly · fluent

Contains returns true if any item satisfies the predicate.

Example: integers

c := collection.New([]int{1, 2, 3, 4, 5})
hasEven := c.Contains(func(v int) bool {
	return v%2 == 0
})
collection.Dump(hasEven)
// true #bool

Example: strings

c2 := collection.New([]string{"apple", "banana", "cherry"})
hasBanana := c2.Contains(func(v string) bool {
	return v == "banana"
})
collection.Dump(hasBanana)
// true #bool

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Carol"},
})

hasBob := users.Contains(func(u User) bool {
	return u.Name == "Bob"
})
collection.Dump(hasBob)
// true #bool

FindWhere · readonly · fluent

FindWhere returns the first item in the collection for which the provided predicate function returns true. This is an alias for FirstWhere(fn) and exists for ergonomic parity with functional languages (JavaScript, Rust, C#, Python) where developers expect a “find” helper.

Example: integers

nums := collection.New([]int{1, 2, 3, 4, 5})

v1, ok1 := nums.FindWhere(func(n int) bool {
	return n == 3
})
collection.Dump(v1, ok1)
// 3    #int
// true #bool

Example: no match

v2, ok2 := nums.FindWhere(func(n int) bool {
	return n > 10
})
collection.Dump(v2, ok2)
// 0     #int
// false #bool

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Charlie"},
})

u, ok3 := users.FindWhere(func(u User) bool {
	return u.ID == 2
})
collection.Dump(u, ok3)
// #collection.User {
//   +ID    => 2   #int
//   +Name  => "Bob" #string
// }
// true #bool

Example: integers - empty collection

empty := collection.New([]int{})

v4, ok4 := empty.FindWhere(func(n int) bool { return n == 1 })
collection.Dump(v4, ok4)
// 0     #int
// false #bool

First · readonly · fluent

First returns the first element in the collection. If the collection is empty, ok will be false.

Example: integers

c := collection.New([]int{10, 20, 30})

v, ok := c.First()
collection.Dump(v, ok)
// 10   #int
// true #bool

Example: strings

c2 := collection.New([]string{"alpha", "beta", "gamma"})

v2, ok2 := c2.First()
collection.Dump(v2, ok2)
// "alpha" #string
// true    #bool

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

u, ok3 := users.First()
collection.Dump(u, ok3)
// #main.User {
//   +ID   => 1      #int
//   +Name => "Alice" #string
// }
// true #bool

Example: integers - empty collection

c3 := collection.New([]int{})
v3, ok4 := c3.First()
collection.Dump(v3, ok4)
// 0    #int
// false #bool

FirstWhere · readonly · fluent

FirstWhere returns the first item in the collection for which the provided predicate function returns true. If no items match, ok=false is returned along with the zero value of T.

nums := collection.New([]int{1, 2, 3, 4, 5})
v, ok := nums.FirstWhere(func(n int) bool {
	return n%2 == 0
})
collection.Dump(v, ok)
// 2 #int
// true #bool

v, ok = nums.FirstWhere(func(n int) bool {
	return n > 10
})
collection.Dump(v, ok)
// 0 #int
// false #bool

IndexWhere · readonly · fluent

IndexWhere returns the index of the first item in the collection for which the provided predicate function returns true. If no item matches, it returns (0, false).

Example: integers

c := collection.New([]int{10, 20, 30, 40})
idx, ok := c.IndexWhere(func(v int) bool { return v == 30 })
collection.Dump(idx, ok)
// 2 true

Example: not found

idx2, ok2 := c.IndexWhere(func(v int) bool { return v == 99 })
collection.Dump(idx2, ok2)
// 0 false

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Carol"},
})

idx3, ok3 := users.IndexWhere(func(u User) bool {
	return u.Name == "Bob"
})

collection.Dump(idx3, ok3)
// 1 true

IsEmpty · readonly · fluent

IsEmpty returns true if the collection has no items.

Example: integers (non-empty)

c := collection.New([]int{1, 2, 3})

empty := c.IsEmpty()
collection.Dump(empty)
// false #bool

Example: strings (empty)

c2 := collection.New([]string{})

empty2 := c2.IsEmpty()
collection.Dump(empty2)
// true #bool

Example: structs (non-empty)

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
})

empty3 := users.IsEmpty()
collection.Dump(empty3)
// false #bool

Example: structs (empty)

none := collection.New([]User{})

empty4 := none.IsEmpty()
collection.Dump(empty4)
// true #bool

Last · readonly · fluent

Last returns the last element in the collection. If the collection is empty, ok will be false.

Example: integers

c := collection.New([]int{10, 20, 30})

v, ok := c.Last()
collection.Dump(v, ok)
// 30   #int
// true #bool

Example: strings

c2 := collection.New([]string{"alpha", "beta", "gamma"})

v2, ok2 := c2.Last()
collection.Dump(v2, ok2)
// "gamma" #string
// true    #bool

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Charlie"},
})

u, ok3 := users.Last()
collection.Dump(u, ok3)
// #main.User {
//   +ID   => 3         #int
//   +Name => "Charlie" #string
// }
// true #bool

Example: empty collection

c3 := collection.New([]int{})

v3, ok4 := c3.Last()
collection.Dump(v3, ok4)
// 0     #int
// false #bool

LastWhere · readonly · fluent

LastWhere returns the last element in the collection that satisfies the predicate fn. If fn is nil, LastWhere returns the final element in the underlying slice. If the collection is empty or no element matches, ok will be false.

Example: integers

c := collection.New([]int{1, 2, 3, 4})

v, ok := c.LastWhere(func(v int, i int) bool {
	return v < 3
})
collection.Dump(v, ok)
// 2    #int
// true #bool

Example: integers without predicate (equivalent to Last())

c2 := collection.New([]int{10, 20, 30, 40})

v2, ok2 := c2.LastWhere(nil)
collection.Dump(v2, ok2)
// 40   #int
// true #bool

Example: strings

c3 := collection.New([]string{"alpha", "beta", "gamma", "delta"})

v3, ok3 := c3.LastWhere(func(s string, i int) bool {
	return strings.HasPrefix(s, "g")
})
collection.Dump(v3, ok3)
// "gamma" #string
// true    #bool

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Alex"},
	{ID: 4, Name: "Brian"},
})

u, ok4 := users.LastWhere(func(u User, i int) bool {
	return strings.HasPrefix(u.Name, "A")
})
collection.Dump(u, ok4)
// #main.User {
//   +ID   => 3        #int
//   +Name => "Alex"  #string
// }
// true #bool

Example: no matching element

c4 := collection.New([]int{5, 6, 7})

v4, ok5 := c4.LastWhere(func(v int, i int) bool {
	return v > 10
})
collection.Dump(v4, ok5)
// 0     #int
// false #bool

Example: empty collection

c5 := collection.New([]int{})

v5, ok6 := c5.LastWhere(nil)
collection.Dump(v5, ok6)
// 0     #int
// false #bool

None · readonly · fluent

None returns true if fn returns false for every item in the collection. If the collection is empty, None returns true.

Example: integers – none even

c := collection.New([]int{1, 3, 5})
noneEven := c.None(func(v int) bool { return v%2 == 0 })
collection.Dump(noneEven)
// true #bool

Example: integers – some even

c2 := collection.New([]int{1, 2, 3})
noneEven2 := c2.None(func(v int) bool { return v%2 == 0 })
collection.Dump(noneEven2)
// false #bool

Example: empty collection

empty := collection.New([]int{})
none := empty.None(func(v int) bool { return v > 0 })
collection.Dump(none)
// true #bool

Serialization

ToJSON · readonly · fluent

ToJSON converts the collection's items into a compact JSON string.

pj1 := collection.New([]string{"a", "b"})
out1, _ := pj1.ToJSON()
fmt.Println(out1)
// ["a","b"]

ToPrettyJSON · readonly · fluent

ToPrettyJSON converts the collection's items into a human-readable, indented JSON string.

pj1 := collection.New([]string{"a", "b"})
out1, _ := pj1.ToPrettyJSON()
fmt.Println(out1)
// [
//  "a",
//  "b"
// ]

Set Operations

Difference · immutable · fluent

Difference returns a new collection containing elements from the first collection that are not present in the second. Order follows the first collection, and duplicates are removed.

Example: integers

a := collection.New([]int{1, 2, 2, 3, 4})
b := collection.New([]int{2, 4})

out := collection.Difference(a, b)
collection.Dump(out.Items())
// #[]int [
//   0 => 1 #int
//   1 => 3 #int
// ]

Example: strings

left := collection.New([]string{"apple", "banana", "cherry"})
right := collection.New([]string{"banana"})

out2 := collection.Difference(left, right)
collection.Dump(out2.Items())
// #[]string [
//   0 => "apple" #string
//   1 => "cherry" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

groupA := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Carol"},
})

groupB := collection.New([]User{
	{ID: 2, Name: "Bob"},
})

out3 := collection.Difference(groupA, groupB)
collection.Dump(out3.Items())
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
//   1 => #main.User {
//     +ID   => 3 #int
//     +Name => "Carol" #string
//   }
// ]

Intersect · immutable · fluent

Intersect returns a new collection containing elements from the second collection that are also present in the first.

Example: integers

a := collection.New([]int{1, 2, 2, 3, 4})
b := collection.New([]int{2, 4, 4, 5})

out := collection.Intersect(a, b)
collection.Dump(out.Items())
// #[]int [
//   0 => 2 #int
//   1 => 4 #int
//   2 => 4 #int
// ]

Example: strings

left := collection.New([]string{"apple", "banana", "cherry"})
right := collection.New([]string{"banana", "date", "cherry", "banana"})

out2 := collection.Intersect(left, right)
collection.Dump(out2.Items())
// #[]string [
//   0 => "banana" #string
//   1 => "cherry" #string
//   2 => "banana" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

groupA := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Carol"},
})

groupB := collection.New([]User{
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Carol"},
	{ID: 4, Name: "Dave"},
})

out3 := collection.Intersect(groupA, groupB)
collection.Dump(out3.Items())
// #[]main.User [
//   0 => #main.User {
//     +ID   => 2 #int
//     +Name => "Bob" #string
//   }
//   1 => #main.User {
//     +ID   => 3 #int
//     +Name => "Carol" #string
//   }
// ]

SymmetricDifference · immutable · fluent

SymmetricDifference returns a new collection containing elements that appear in exactly one of the two collections. Order follows the first collection for its unique items, then the second for its unique items. Duplicates are removed.

Example: integers

a := collection.New([]int{1, 2, 3, 3})
b := collection.New([]int{3, 4, 4, 5})

out := collection.SymmetricDifference(a, b)
collection.Dump(out.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 4 #int
//   3 => 5 #int
// ]

Example: strings

left := collection.New([]string{"apple", "banana"})
right := collection.New([]string{"banana", "date"})

out2 := collection.SymmetricDifference(left, right)
collection.Dump(out2.Items())
// #[]string [
//   0 => "apple" #string
//   1 => "date" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

groupA := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

groupB := collection.New([]User{
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Carol"},
})

out3 := collection.SymmetricDifference(groupA, groupB)
collection.Dump(out3.Items())
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
//   1 => #main.User {
//     +ID   => 3 #int
//     +Name => "Carol" #string
//   }
// ]

Union · immutable · fluent

Union returns a new collection containing the unique elements from both collections. Items from the first collection are kept in order, followed by items from the second that were not already present.

Example: integers

a := collection.New([]int{1, 2, 2, 3})
b := collection.New([]int{3, 4, 4, 5})

out := collection.Union(a, b)
collection.Dump(out.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
//   3 => 4 #int
//   4 => 5 #int
// ]

Example: strings

left := collection.New([]string{"apple", "banana"})
right := collection.New([]string{"banana", "date"})

out2 := collection.Union(left, right)
collection.Dump(out2.Items())
// #[]string [
//   0 => "apple" #string
//   1 => "banana" #string
//   2 => "date" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

groupA := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

groupB := collection.New([]User{
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Carol"},
})

out3 := collection.Union(groupA, groupB)
collection.Dump(out3.Items())
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
//   1 => #main.User {
//     +ID   => 2 #int
//     +Name => "Bob" #string
//   }
//   2 => #main.User {
//     +ID   => 3 #int
//     +Name => "Carol" #string
//   }
// ]

Unique · immutable · fluent

Unique returns a new collection with duplicate items removed, based on the equality function eq. The first occurrence of each unique value is kept, and order is preserved.

Example: integers

c1 := collection.New([]int{1, 2, 2, 3, 4, 4, 5})
out1 := c1.Unique(func(a, b int) bool { return a == b })
collection.Dump(out1.Items())
// #[]int [
//	0 => 1 #int
//	1 => 2 #int
//	2 => 3 #int
//	3 => 4 #int
//	4 => 5 #int
// ]

Example: strings (case-insensitive uniqueness)

c2 := collection.New([]string{"A", "a", "B", "b", "A"})
out2 := c2.Unique(func(a, b string) bool {
	return strings.EqualFold(a, b)
})
collection.Dump(out2.Items())
// #[]string [
//	0 => "A" #string
//	1 => "B" #string
// ]

Example: structs (unique by ID)

type User struct {
	ID   int
	Name string
}

c3 := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 1, Name: "Alice Duplicate"},
})

out3 := c3.Unique(func(a, b User) bool {
	return a.ID == b.ID
})

collection.Dump(out3.Items())
// #[]collection.User [
//	0 => {ID:1 Name:"Alice"} #collection.User
//	1 => {ID:2 Name:"Bob"}   #collection.User
// ]

UniqueBy · immutable · fluent

UniqueBy returns a new collection containing only the first occurrence of each element as determined by keyFn.

Example: structs – unique by ID

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 1, Name: "Alice Duplicate"},
})

out := collection.UniqueBy(users, func(u User) int { return u.ID })
collection.Dump(out.Items())
// #[]collection.User [
//   0 => {ID:1 Name:"Alice"} #collection.User
//   1 => {ID:2 Name:"Bob"}   #collection.User
// ]

Example: strings – case-insensitive uniqueness

values := collection.New([]string{"A", "a", "B", "b", "A"})

out2 := collection.UniqueBy(values, func(s string) string {
	return strings.ToLower(s)
})

collection.Dump(out2.Items())
// #[]string [
//   0 => "A" #string
//   1 => "B" #string
// ]

Example: integers – identity key

nums := collection.New([]int{3, 1, 2, 1, 3})

out3 := collection.UniqueBy(nums, func(v int) int { return v })
collection.Dump(out3.Items())
// #[]int [
//   0 => 3 #int
//   1 => 1 #int
//   2 => 2 #int
// ]

UniqueComparable · immutable · fluent

UniqueComparable returns a new collection with duplicate comparable items removed. The first occurrence of each value is kept, and order is preserved. This is a faster, allocation-friendly path for comparable types.

Example: integers

c := collection.New([]int{1, 2, 2, 3, 4, 4, 5})
out := collection.UniqueComparable(c)
collection.Dump(out.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
//   3 => 4 #int
//   4 => 5 #int
// ]

Example: strings

c2 := collection.New([]string{"A", "a", "B", "B"})
out2 := collection.UniqueComparable(c2)
collection.Dump(out2.Items())
// #[]string [
//   0 => "A" #string
//   1 => "a" #string
//   2 => "B" #string
// ]

Slicing

Chunk · readonly · fluent

Chunk splits the collection into chunks of the given size. The final chunk may be smaller if len(items) is not divisible by size.

Example: integers

c := collection.New([]int{1, 2, 3, 4, 5}).Chunk(2)
collection.Dump(c)

// #[][]int [
//  0 => #[]int [
//    0 => 1 #int
//    1 => 2 #int
//  ]
//  1 => #[]int [
//    0 => 3 #int
//    1 => 4 #int
//  ]
//  2 => #[]int [
//    0 => 5 #int
//  ]
//]

Example: structs

type User struct {
	ID   int
	Name string
}

users := []User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Carol"},
	{ID: 4, Name: "Dave"},
}

userChunks := collection.New(users).Chunk(2)
collection.Dump(userChunks)

// Dump output will show [][]User grouped in size-2 chunks, e.g.:
// #[][]main.User [
//  0 => #[]main.User [
//    0 => #main.User {
//      +ID   => 1 #int
//      +Name => "Alice" #string
//    }
//    1 => #main.User {
//      +ID   => 2 #int
//      +Name => "Bob" #string
//    }
//  ]
//  1 => #[]main.User [
//    0 => #main.User {
//      +ID   => 3 #int
//      +Name => "Carol" #string
//    }
//    1 => #main.User {
//      +ID   => 4 #int
//      +Name => "Dave" #string
//    }
//  ]
//]

Filter · mutable · fluent

Filter keeps only the elements for which fn returns true. This method mutates the collection in place and returns the same instance.

Example: integers

c := collection.New([]int{1, 2, 3, 4})
c.Filter(func(v int) bool {
	return v%2 == 0
})
collection.Dump(c.Items())
// #[]int [
//   0 => 2 #int
//   1 => 4 #int
// ]

Example: strings

c2 := collection.New([]string{"apple", "banana", "cherry", "avocado"})
c2.Filter(func(v string) bool {
	return strings.HasPrefix(v, "a")
})
collection.Dump(c2.Items())
// #[]string [
//   0 => "apple" #string
//   1 => "avocado" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Andrew"},
	{ID: 4, Name: "Carol"},
})

users.Filter(func(u User) bool {
	return strings.HasPrefix(u.Name, "A")
})

collection.Dump(users.Items())
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
//   1 => #main.User {
//     +ID   => 3 #int
//     +Name => "Andrew" #string
//   }
// ]

Partition · immutable · fluent

Partition splits the collection into two new collections based on predicate fn. The first collection contains items where fn returns true; the second contains items where fn returns false. Order is preserved within each partition.

Example: integers - even/odd

nums := collection.New([]int{1, 2, 3, 4, 5})
evens, odds := nums.Partition(func(n int) bool {
	return n%2 == 0
})
collection.Dump(evens.Items(), odds.Items())
// #[]int [
//   0 => 2 #int
//   1 => 4 #int
// ]
// #[]int [
//   0 => 1 #int
//   1 => 3 #int
//   2 => 5 #int
// ]

Example: strings - prefix match

words := collection.New([]string{"go", "gopher", "rust", "ruby"})
goWords, other := words.Partition(func(s string) bool {
	return strings.HasPrefix(s, "go")
})
collection.Dump(goWords.Items(), other.Items())
// #[]string [
//   0 => "go" #string
//   1 => "gopher" #string
// ]
// #[]string [
//   0 => "rust" #string
//   1 => "ruby" #string
// ]

Example: structs - active vs inactive

type User struct {
	Name   string
	Active bool
}

users := collection.New([]User{
	{Name: "Alice", Active: true},
	{Name: "Bob", Active: false},
	{Name: "Carol", Active: true},
})

active, inactive := users.Partition(func(u User) bool {
	return u.Active
})

collection.Dump(active.Items(), inactive.Items())
// #[]main.User [
//   0 => #main.User {
//     +Name   => "Alice" #string
//     +Active => true #bool
//   }
//   1 => #main.User {
//     +Name   => "Carol" #string
//     +Active => true #bool
//   }
// ]
// #[]main.User [
//   0 => #main.User {
//     +Name   => "Bob" #string
//     +Active => false #bool
//   }
// ]

Pop · mutable · fluent

Pop returns the last item and a new collection with that item removed. The original collection remains unchanged.

Example: integers

c := collection.New([]int{1, 2, 3})
item, rest := c.Pop()
collection.Dump(item, rest.Items())
// 3 #int
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
// ]

Example: strings

c2 := collection.New([]string{"a", "b", "c"})
item2, rest2 := c2.Pop()
collection.Dump(item2, rest2.Items())
// "c" #string
// #[]string [
//   0 => "a" #string
//   1 => "b" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

item3, rest3 := users.Pop()
collection.Dump(item3, rest3.Items())
// #main.User {
//   +ID   => 2 #int
//   +Name => "Bob" #string
// }
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
// ]

Example: empty collection

empty := collection.New([]int{})
item4, rest4 := empty.Pop()
collection.Dump(item4, rest4.Items())
// 0 #int
// #[]int [
// ]

PopN · mutable · fluent

PopN removes and returns the last n items as a new collection, and returns a second collection containing the remaining items.

Example: integers – pop 2

c := collection.New([]int{1, 2, 3, 4})
popped, rest := c.PopN(2)
collection.Dump(popped.Items(), rest.Items())
// #[]int [
//   0 => 4 #int
//   1 => 3 #int
// ]
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
// ]

Example: strings – pop 1

c2 := collection.New([]string{"a", "b", "c"})
popped2, rest2 := c2.PopN(1)
collection.Dump(popped2.Items(), rest2.Items())
// #[]string [
//   0 => "c" #string
// ]
// #[]string [
//   0 => "a" #string
//   1 => "b" #string
// ]

Example: structs – pop 2

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Carol"},
})

popped3, rest3 := users.PopN(2)
collection.Dump(popped3.Items(), rest3.Items())
// #[]main.User [
//   0 => #main.User {
//     +ID   => 3 #int
//     +Name => "Carol" #string
//   }
//   1 => #main.User {
//     +ID   => 2 #int
//     +Name => "Bob" #string
//   }
// ]
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
// ]

Example: integers - n <= 0 → returns empty popped + original collection

c3 := collection.New([]int{1, 2, 3})
popped4, rest4 := c3.PopN(0)
collection.Dump(popped4.Items(), rest4.Items())
// #[]int [
// ]
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
// ]

Example: strings - n exceeds length → all items popped, rest empty

c4 := collection.New([]string{"x", "y"})
popped5, rest5 := c4.PopN(10)
collection.Dump(popped5.Items(), rest5.Items())
// #[]string [
//   0 => "y" #string
//   1 => "x" #string
// ]
// #[]string [
// ]

Skip · immutable · fluent

Skip returns a new collection with the first n items skipped. If n is less than or equal to zero, Skip returns the full collection. If n is greater than or equal to the collection length, Skip returns an empty collection.

Example: integers

c := collection.New([]int{1, 2, 3, 4, 5})
out := c.Skip(2)
collection.Dump(out.Items())
// #[]int [
//   0 => 3 #int
//   1 => 4 #int
//   2 => 5 #int
// ]

Example: skip none

out2 := c.Skip(0)
collection.Dump(out2.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
//   3 => 4 #int
//   4 => 5 #int
// ]

Example: skip all

out3 := c.Skip(10)
collection.Dump(out3.Items())
// #[]int []

Example: structs

type User struct {
	ID int
}

users := collection.New([]User{
	{ID: 1},
	{ID: 2},
	{ID: 3},
})

out4 := users.Skip(1)
collection.Dump(out4.Items())
// []main.User [
//  0 => #main.User {
//    +ID => 2 #int
//  }
//  1 => #main.User {
//    +ID => 3 #int
//  }
// ]

SkipLast · immutable · fluent

SkipLast returns a new collection with the last n items skipped. If n is less than or equal to zero, SkipLast returns the full collection. If n is greater than or equal to the collection length, SkipLast returns an empty collection.

Example: integers

c := collection.New([]int{1, 2, 3, 4, 5})
out := c.SkipLast(2)
collection.Dump(out.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
// ]

Example: skip none

out2 := c.SkipLast(0)
collection.Dump(out2.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
//   3 => 4 #int
//   4 => 5 #int
// ]

Example: skip all

out3 := c.SkipLast(10)
collection.Dump(out3.Items())
// #[]int []

Example: structs

type User struct {
	ID int
}

users := collection.New([]User{
	{ID: 1},
	{ID: 2},
	{ID: 3},
})

out4 := users.SkipLast(1)
collection.Dump(out4.Items())
// #[]collection.User [
//   0 => {ID:1} #collection.User
//   1 => {ID:2} #collection.User
// ]

Take · immutable · fluent

Take returns a new collection containing the first n items when n > 0, or the last |n| items when n < 0.

Example: integers - take first 3

c1 := collection.New([]int{0, 1, 2, 3, 4, 5})
out1 := c1.Take(3)
collection.Dump(out1.Items())
// #[]int [
//	0 => 0 #int
//	1 => 1 #int
//	2 => 2 #int
// ]

Example: integers - take last 2 (negative n)

c2 := collection.New([]int{0, 1, 2, 3, 4, 5})
out2 := c2.Take(-2)
collection.Dump(out2.Items())
// #[]int [
//	0 => 4 #int
//	1 => 5 #int
// ]

Example: integers - n exceeds length → whole collection

c3 := collection.New([]int{10, 20})
out3 := c3.Take(10)
collection.Dump(out3.Items())
// #[]int [
//	0 => 10 #int
//	1 => 20 #int
// ]

Example: integers - zero → empty

c4 := collection.New([]int{1, 2, 3})
out4 := c4.Take(0)
collection.Dump(out4.Items())
// #[]int [
// ]

TakeLast · immutable · fluent

TakeLast returns a new collection containing the last n items. If n is less than or equal to zero, TakeLast returns an empty collection. If n is greater than or equal to the collection length, TakeLast returns the full collection.

Example: integers

c := collection.New([]int{1, 2, 3, 4, 5})
out := c.TakeLast(2)
collection.Dump(out.Items())
// #[]int [
//   0 => 4 #int
//   1 => 5 #int
// ]

Example: take none

out2 := c.TakeLast(0)
collection.Dump(out2.Items())
// #[]int []

Example: take all

out3 := c.TakeLast(10)
collection.Dump(out3.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
//   3 => 4 #int
//   4 => 5 #int
// ]

Example: structs

type User struct {
	ID int
}

users := collection.New([]User{
	{ID: 1},
	{ID: 2},
	{ID: 3},
})

out4 := users.TakeLast(1)
collection.Dump(out4.Items())
// #[]collection.User [
//   0 => {ID:3} #collection.User
// ]

TakeUntil · immutable · fluent

TakeUntil returns items until the first element equals value. The matching item is NOT included.

Example: integers - stop at value 3

c4 := collection.New([]int{1, 2, 3, 4})
out4 := collection.TakeUntil(c4, 3)
collection.Dump(out4.Items())
// #[]int [
//	0 => 1 #int
//	1 => 2 #int
// ]

Example: strings - value never appears → full slice

c5 := collection.New([]string{"a", "b", "c"})
out5 := collection.TakeUntil(c5, "x")
collection.Dump(out5.Items())
// #[]string [
//	0 => "a" #string
//	1 => "b" #string
//	2 => "c" #string
// ]

Example: integers - match is first item → empty result

c6 := collection.New([]int{9, 10, 11})
out6 := collection.TakeUntil(c6, 9)
collection.Dump(out6.Items())
// #[]int [
// ]

TakeUntilFn · immutable · fluent

TakeUntilFn returns items until the predicate function returns true. The matching item is NOT included.

Example: integers - stop when value >= 3

c1 := collection.New([]int{1, 2, 3, 4})
out1 := c1.TakeUntilFn(func(v int) bool { return v >= 3 })
collection.Dump(out1.Items())
// #[]int [
//	0 => 1 #int
//	1 => 2 #int
// ]

Example: integers - predicate immediately true → empty result

c2 := collection.New([]int{10, 20, 30})
out2 := c2.TakeUntilFn(func(v int) bool { return v < 50 })
collection.Dump(out2.Items())
// #[]int [
// ]

Example: integers - no match → full list returned

c3 := collection.New([]int{1, 2, 3})
out3 := c3.TakeUntilFn(func(v int) bool { return v == 99 })
collection.Dump(out3.Items())
// #[]int [
//	0 => 1 #int
//	1 => 2 #int
//	2 => 3 #int
// ]

Window · allocates · fluent

Window returns overlapping (or stepped) windows of the collection. Each window is a slice of length size; iteration advances by step (default 1 if step <= 0). Windows that are shorter than size are omitted.

Example: integers - step 1

nums := collection.New([]int{1, 2, 3, 4, 5})
win := collection.Window(nums, 3, 1)
collection.Dump(win.Items())
// #[][]int [
//   0 => #[]int [
//     0 => 1 #int
//     1 => 2 #int
//     2 => 3 #int
//   ]
//   1 => #[]int [
//     0 => 2 #int
//     1 => 3 #int
//     2 => 4 #int
//   ]
//   2 => #[]int [
//     0 => 3 #int
//     1 => 4 #int
//     2 => 5 #int
//   ]
// ]

Example: strings - step 2

words := collection.New([]string{"a", "b", "c", "d", "e"})
win2 := collection.Window(words, 2, 2)
collection.Dump(win2.Items())
// #[][]string [
//   0 => #[]string [
//     0 => "a" #string
//     1 => "b" #string
//   ]
//   1 => #[]string [
//     0 => "c" #string
//     1 => "d" #string
//   ]
// ]

Example: structs

type Point struct {
	X int
	Y int
}

points := collection.New([]Point{
	{X: 0, Y: 0},
	{X: 1, Y: 1},
	{X: 2, Y: 4},
	{X: 3, Y: 9},
})

win3 := collection.Window(points, 2, 1)
collection.Dump(win3.Items())
// #[][]main.Point [
//   0 => #[]main.Point [
//     0 => #main.Point {
//       +X => 0 #int
//       +Y => 0 #int
//     }
//     1 => #main.Point {
//       +X => 1 #int
//       +Y => 1 #int
//     }
//   ]
//   1 => #[]main.Point [
//     0 => #main.Point {
//       +X => 1 #int
//       +Y => 1 #int
//     }
//     1 => #main.Point {
//       +X => 2 #int
//       +Y => 4 #int
//     }
//   ]
//   2 => #[]main.Point [
//     0 => #main.Point {
//       +X => 2 #int
//       +Y => 4 #int
//     }
//     1 => #main.Point {
//       +X => 3 #int
//       +Y => 9 #int
//     }
//   ]
// ]

Transformation

Append · immutable · fluent

Append returns a new collection with the given values appended.

Example: integers

c := collection.New([]int{1, 2})
c.Append(3, 4).Dump()
// #[]int [
//  0 => 1 #int
//  1 => 2 #int
//  2 => 3 #int
//  3 => 4 #int
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

users.Append(
	User{ID: 3, Name: "Carol"},
	User{ID: 4, Name: "Dave"},
).Dump()

// #[]main.User [
//  0 => #main.User {
//    +ID   => 1 #int
//    +Name => "Alice" #string
//  }
//  1 => #main.User {
//    +ID   => 2 #int
//    +Name => "Bob" #string
//  }
//  2 => #main.User {
//    +ID   => 3 #int
//    +Name => "Carol" #string
//  }
//  3 => #main.User {
//    +ID   => 4 #int
//    +Name => "Dave" #string
//  }
// ]

Concat · mutable · fluent

Concat appends the values from the given slice onto the end of the collection,

c := collection.New([]string{"John Doe"})
concatenated := c.
	Concat([]string{"Jane Doe"}).
	Concat([]string{"Johnny Doe"}).
	Items()
collection.Dump(concatenated)

// #[]string [
//  0 => "John Doe" #string
//  1 => "Jane Doe" #string
//  2 => "Johnny Doe" #string
// ]

Each · immutable · fluent

Each runs fn for every item in the collection and returns the same collection, so it can be used in chains for side effects (logging, debugging, etc.).

Example: integers

c := collection.New([]int{1, 2, 3})

sum := 0
c.Each(func(v int) {
	sum += v
})

collection.Dump(sum)
// 6 #int

Example: strings

c2 := collection.New([]string{"apple", "banana", "cherry"})

var out []string
c2.Each(func(s string) {
	out = append(out, strings.ToUpper(s))
})

collection.Dump(out)
// #[]string [
//   0 => "APPLE"  #string
//   1 => "BANANA" #string
//   2 => "CHERRY" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Charlie"},
})

var names []string
users.Each(func(u User) {
	names = append(names, u.Name)
})

collection.Dump(names)
// #[]string [
//   0 => "Alice"   #string
//   1 => "Bob"     #string
//   2 => "Charlie" #string
// ]

Map · immutable · fluent

Map applies a same-type transformation and returns a new collection.

Example: integers

c := collection.New([]int{1, 2, 3})

mapped := c.Map(func(v int) int {
	return v * 10
})

collection.Dump(mapped.Items())
// #[]int [
//   0 => 10 #int
//   1 => 20 #int
//   2 => 30 #int
// ]

Example: strings

c2 := collection.New([]string{"apple", "banana", "cherry"})

upper := c2.Map(func(s string) string {
	return strings.ToUpper(s)
})

collection.Dump(upper.Items())
// #[]string [
//   0 => "APPLE"  #string
//   1 => "BANANA" #string
//   2 => "CHERRY" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

updated := users.Map(func(u User) User {
	u.Name = strings.ToUpper(u.Name)
	return u
})

collection.Dump(updated.Items())
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1        #int
//     +Name => "ALICE"  #string
//   }
//   1 => #main.User {
//     +ID   => 2        #int
//     +Name => "BOB"    #string
//   }
// ]

MapTo · immutable · fluent

MapTo maps a Collection[T] to a Collection[R] using fn(T) R.

Example: integers - extract parity label

nums := collection.New([]int{1, 2, 3, 4})
parity := collection.MapTo(nums, func(n int) string {
	if n%2 == 0 {
		return "even"
	}
	return "odd"
})
collection.Dump(parity.Items())
// #[]string [
//   0 => "odd" #string
//   1 => "even" #string
//   2 => "odd" #string
//   3 => "even" #string
// ]

Example: strings - length of each value

words := collection.New([]string{"go", "forj", "rocks"})
lengths := collection.MapTo(words, func(s string) int {
	return len(s)
})
collection.Dump(lengths.Items())
// #[]int [
//   0 => 2 #int
//   1 => 4 #int
//   2 => 5 #int
// ]

Example: structs - MapTo a field

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

names := collection.MapTo(users, func(u User) string {
	return u.Name
})

collection.Dump(names.Items())
// #[]string [
//   0 => "Alice" #string
//   1 => "Bob" #string
// ]

Merge · mutable · fluent

Merge merges the given data into the current collection.

Example: integers - merging slices

ints := collection.New([]int{1, 2})
extra := []int{3, 4}
// Merge the extra slice into the ints collection
merged1 := ints.Merge(extra)
collection.Dump(merged1.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
//   3 => 4 #int
// ]

Example: strings - merging another collection

strs := collection.New([]string{"a", "b"})
more := collection.New([]string{"c", "d"})

merged2 := strs.Merge(more)
collection.Dump(merged2.Items())
// #[]string [
//   0 => "a" #string
//   1 => "b" #string
//   2 => "c" #string
//   3 => "d" #string
// ]

Example: structs - merging struct slices

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

moreUsers := []User{
	{ID: 3, Name: "Carol"},
	{ID: 4, Name: "Dave"},
}

merged3 := users.Merge(moreUsers)
collection.Dump(merged3.Items())
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
//   1 => #main.User {
//     +ID   => 2 #int
//     +Name => "Bob" #string
//   }
//   2 => #main.User {
//     +ID   => 3 #int
//     +Name => "Carol" #string
//   }
//   3 => #main.User {
//     +ID   => 4 #int
//     +Name => "Dave" #string
//   }
// ]

Multiply · mutable · fluent

Multiply creates n copies of all items in the collection and returns a new collection.

Example: integers

ints := collection.New([]int{1, 2})
out := ints.Multiply(3)
collection.Dump(out.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 1 #int
//   3 => 2 #int
//   4 => 1 #int
//   5 => 2 #int
// ]

Example: strings

strs := collection.New([]string{"a", "b"})
out2 := strs.Multiply(2)
collection.Dump(out2.Items())
// #[]string [
//   0 => "a" #string
//   1 => "b" #string
//   2 => "a" #string
//   3 => "b" #string
// ]

Example: structs

type User struct {
	Name string
}

users := collection.New([]User{{Name: "Alice"}, {Name: "Bob"}})
out3 := users.Multiply(2)
collection.Dump(out3.Items())
// #[]main.User [
//   0 => #main.User {
//     +Name => "Alice" #string
//   }
//   1 => #main.User {
//     +Name => "Bob" #string
//   }
//   2 => #main.User {
//     +Name => "Alice" #string
//   }
//   3 => #main.User {
//     +Name => "Bob" #string
//   }
// ]

Example: multiplying by zero or negative returns empty

none := ints.Multiply(0)
collection.Dump(none.Items())
// #[]int [
// ]

Pipe · readonly · fluent

Pipe passes the entire collection into the given function and returns the function's result.

Example: integers – computing a sum

c := collection.New([]int{1, 2, 3})
sum := c.Pipe(func(col *collection.Collection[int]) any {
	total := 0
	for _, v := range col.Items() {
		total += v
	}
	return total
})
collection.Dump(sum)
// 6 #int

Example: strings – joining values

c2 := collection.New([]string{"a", "b", "c"})
joined := c2.Pipe(func(col *collection.Collection[string]) any {
	out := ""
	for _, v := range col.Items() {
		out += v
	}
	return out
})
collection.Dump(joined)
// "abc" #string

Example: structs – extracting just the names

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

names := users.Pipe(func(col *collection.Collection[User]) any {
	result := make([]string, 0, len(col.Items()))
	for _, u := range col.Items() {
		result = append(result, u.Name)
	}
	return result
})

collection.Dump(names)
// #[]string [
//   0 => "Alice" #string
//   1 => "Bob" #string
// ]

Pluck · immutable · fluent

Pluck is an alias for MapTo with a more semantic name when projecting fields. It extracts a single field or computed value from every element and returns a new typed collection.

Example: integers - extract parity label

nums := collection.New([]int{1, 2, 3, 4})
parity := collection.Pluck(nums, func(n int) string {
	if n%2 == 0 {
		return "even"
	}
	return "odd"
})
collection.Dump(parity.Items())
// #[]string [
//   0 => "odd" #string
//   1 => "even" #string
//   2 => "odd" #string
//   3 => "even" #string
// ]

Example: strings - length of each value

words := collection.New([]string{"go", "forj", "rocks"})
lengths := collection.Pluck(words, func(s string) int {
	return len(s)
})
collection.Dump(lengths.Items())
// #[]int [
//   0 => 2 #int
//   1 => 4 #int
//   2 => 5 #int
// ]

Example: structs - pluck a field

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

names := collection.Pluck(users, func(u User) string {
	return u.Name
})

collection.Dump(names.Items())
// #[]string [
//   0 => "Alice" #string
//   1 => "Bob" #string
// ]

Prepend · mutable · fluent

Prepend returns a new collection with the given values added to the beginning of the collection.

Example: integers

c := collection.New([]int{3, 4})
newC := c.Prepend(1, 2)
collection.Dump(newC.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
//   3 => 4 #int
// ]

Example: strings

letters := collection.New([]string{"c", "d"})
out := letters.Prepend("a", "b")
collection.Dump(out.Items())
// #[]string [
//   0 => "a" #string
//   1 => "b" #string
//   2 => "c" #string
//   3 => "d" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 2, Name: "Bob"},
})

out2 := users.Prepend(User{ID: 1, Name: "Alice"})
collection.Dump(out2.Items())
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
//   1 => #main.User {
//     +ID   => 2 #int
//     +Name => "Bob" #string
//   }
// ]

Example: integers - Prepending into an empty collection

empty := collection.New([]int{})
out3 := empty.Prepend(9, 8)
collection.Dump(out3.Items())
// #[]int [
//   0 => 9 #int
//   1 => 8 #int
// ]

Example: integers - Prepending no values → returns a copy of original

c2 := collection.New([]int{1, 2})
out4 := c2.Prepend()
collection.Dump(out4.Items())
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
// ]

Push · immutable · fluent

Push returns a new collection with the given values appended.

nums := collection.New([]int{1, 2}).Push(3, 4)
nums.Dump()
// #[]int [
//  0 => 1 #int
//  1 => 2 #int
//  2 => 3 #int
//  3 => 4 #int
// ]

// Complex type (structs)
type User struct {
	Name string
	Age  int
}

users := collection.New([]User{
	{Name: "Alice", Age: 30},
	{Name: "Bob", Age: 25},
}).Push(
	User{Name: "Carol", Age: 40},
	User{Name: "Dave", Age: 20},
)
users.Dump()
// #[]main.User [
//  0 => #main.User {
//    +Name => "Alice" #string
//    +Age  => 30 #int
//  }
//  1 => #main.User {
//    +Name => "Bob" #string
//    +Age  => 25 #int
//  }
//  2 => #main.User {
//    +Name => "Carol" #string
//    +Age  => 40 #int
//  }
//  3 => #main.User {
//    +Name => "Dave" #string
//    +Age  => 20 #int
//  }
// ]

Tap · immutable · fluent

Tap invokes fn with the collection pointer for side effects (logging, debugging, inspection) and returns the same collection to allow chaining.

Example: integers - capture intermediate state during a chain

captured1 := []int{}
c1 := collection.New([]int{3, 1, 2}).
	Sort(func(a, b int) bool { return a < b }). // → [1, 2, 3]
	Tap(func(col *collection.Collection[int]) {
		captured1 = append([]int(nil), col.Items()...) // snapshot copy
	}).
	Filter(func(v int) bool { return v >= 2 }).
	Dump()
	// #[]int [
	//  0 => 2 #int
	//  1 => 3 #int
	// ]

// Use BOTH variables so nothing is "declared and not used"
collection.Dump(c1.Items())
collection.Dump(captured1)
// c1 → #[]int [2,3]
// captured1 → #[]int [1,2,3]

Example: integers - tap for debugging without changing flow

c2 := collection.New([]int{10, 20, 30}).
	Tap(func(col *collection.Collection[int]) {
		collection.Dump(col.Items())
	}).
	Filter(func(v int) bool { return v > 10 })

collection.Dump(c2.Items()) // ensures c2 is used

Example: structs - Tap with struct collection

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

users2 := users.Tap(func(col *collection.Collection[User]) {
	collection.Dump(col.Items())
})

collection.Dump(users2.Items()) // ensures users2 is used

Times · immutable · fluent

Times creates a new collection by calling fn(i) for i = 1..count. This mirrors Laravel's Collection::times(), which is 1-indexed.

Example: integers - double each index

cTimes1 := collection.Times(5, func(i int) int {
	return i * 2
})
collection.Dump(cTimes1.Items())
// #[]int [
//	0 => 2  #int
//	1 => 4  #int
//	2 => 6  #int
//	3 => 8  #int
//	4 => 10 #int
// ]

Example: strings

cTimes2 := collection.Times(3, func(i int) string {
	return fmt.Sprintf("item-%d", i)
})
collection.Dump(cTimes2.Items())
// #[]string [
//	0 => "item-1" #string
//	1 => "item-2" #string
//	2 => "item-3" #string
// ]

Example: structs

type Point struct {
	X int
	Y int
}

cTimes3 := collection.Times(4, func(i int) Point {
	return Point{X: i, Y: i * i}
})
collection.Dump(cTimes3.Items())
// #[]main.Point [
//	0 => #main.Point {
//		+X => 1 #int
//		+Y => 1 #int
//	}
//	1 => #main.Point {
//		+X => 2 #int
//		+Y => 4 #int
//	}
//	2 => #main.Point {
//		+X => 3 #int
//		+Y => 9 #int
//	}
//	3 => #main.Point {
//		+X => 4 #int
//		+Y => 16 #int
//	}
// ]

Transform · mutable · fluent

Transform applies fn to every item in place, mutating the collection.

Example: integers

c1 := collection.New([]int{1, 2, 3})
c1.Transform(func(v int) int { return v * 2 })
collection.Dump(c1.Items())
// #[]int [
//	0 => 2 #int
//	1 => 4 #int
//	2 => 6 #int
// ]

Example: strings

c2 := collection.New([]string{"a", "b", "c"})
c2.Transform(func(s string) string { return strings.ToUpper(s) })
collection.Dump(c2.Items())
// #[]string [
//	0 => "A" #string
//	1 => "B" #string
//	2 => "C" #string
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

c3 := collection.New([]User{
	{ID: 1, Name: "alice"},
	{ID: 2, Name: "bob"},
})

c3.Transform(func(u User) User {
	u.Name = strings.ToUpper(u.Name)
	return u
})

collection.Dump(c3.Items())
// #[]collection.User [
//	0 => {ID:1 Name:"ALICE"} #collection.User
//	1 => {ID:2 Name:"BOB"}   #collection.User
// ]

Zip · immutable · fluent

Zip combines two collections element-wise into a collection of tuples. The resulting length is the smaller of the two inputs.

Example: integers and strings

nums := collection.New([]int{1, 2, 3})
words := collection.New([]string{"one", "two"})

out := collection.Zip(nums, words)
collection.Dump(out.Items())
// #[]collection.Tuple[int,string] [
//   0 => #collection.Tuple[int,string] {
//     +First  => 1 #int
//     +Second => "one" #string
//   }
//   1 => #collection.Tuple[int,string] {
//     +First  => 2 #int
//     +Second => "two" #string
//   }
// ]

Example: structs

type User struct {
	ID   int
	Name string
}

users := collection.New([]User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
})

roles := collection.New([]string{"admin", "user", "extra"})

out2 := collection.Zip(users, roles)
collection.Dump(out2.Items())
// #[]collection.Tuple[main.User,string] [
//   0 => #collection.Tuple[main.User,string] {
//     +First  => #main.User {
//       +ID   => 1 #int
//       +Name => "Alice" #string
//     }
//     +Second => "admin" #string
//   }
//   1 => #collection.Tuple[main.User,string] {
//     +First  => #main.User {
//       +ID   => 2 #int
//       +Name => "Bob" #string
//     }
//     +Second => "user" #string
//   }
// ]

ZipWith · immutable · fluent

ZipWith combines two collections element-wise using combiner fn. The resulting length is the smaller of the two inputs.

Example: sum ints

a := collection.New([]int{1, 2, 3})
b := collection.New([]int{10, 20})

out := collection.ZipWith(a, b, func(x, y int) int {
	return x + y
})

collection.Dump(out.Items())
// #[]int [
//   0 => 11 #int
//   1 => 22 #int
// ]

Example: format strings

names := collection.New([]string{"alice", "bob"})
roles := collection.New([]string{"admin", "user", "extra"})

out2 := collection.ZipWith(names, roles, func(name, role string) string {
	return name + ":" + role
})

collection.Dump(out2.Items())
// #[]string [
//   0 => "alice:admin" #string
//   1 => "bob:user" #string
// ]

Example: structs

type User struct {
	Name string
}

type Role struct {
	Title string
}

users := collection.New([]User{{Name: "Alice"}, {Name: "Bob"}})
roles2 := collection.New([]Role{{Title: "admin"}})

out3 := collection.ZipWith(users, roles2, func(u User, r Role) string {
	return u.Name + " -> " + r.Title
})

collection.Dump(out3.Items())
// #[]string [
//   0 => "Alice -> admin" #string
// ]

About

A fluent, Laravel-inspired Collection library for Go - with chaining, higher-order functions, and expressive data manipulation.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages