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 collections for Go - with generics, chainable pipelines, and expressive data transforms. Inspired by Laravel, designed to feel natural in Go.

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 data with zero dependencies - familiar to Go developers, pleasant to use everywhere.

Features

  • Fluent chaining - pipeline your operations like Laravel Collections
  • Fully generic (Collection[T]) - no reflection, no interface{}
  • Minimal dependencies - small footprint (godump for debugging)
  • Minimal allocations - slice views where possible; in-place ops reuse backing storage when semantics allow
  • Map / Filter / Reduce - clean functional transforms
  • First / Last / FirstWhere / IndexWhere / Contains helpers
  • Sort, GroupBy, Chunk, and more
  • Borrow-by-default - no defensive copies unless you ask for them
  • 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 may be limited 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

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.

Below - A fixed ~24B allocation is the cost of the Collection wrapper (one-time per pipeline), not additional work per operation

The below tables are automatically generated from ./docs/bench/main.go.

Full raw tables: see BENCHMARKS.md.

Read-only scalar ops (wrapper overhead only)

Op Speed vs lo Memory Allocs
All +24B +1
Any +24B +1
None +24B +1
First +24B +1
Last +24B +1
FirstWhere +24B +1
IndexWhere +24B +1
Contains +24B +1
Reduce (sum) +24B +1
Sum +32B +2
Min +32B +2
Max +32B +2
Each +24B +1

Transforming ops

Op Speed vs lo Memory Allocs
Chunk 7.64x -8.0KB -49
Take +48B +2
Skip 71.40x -8.2KB
SkipLast 71.90x -8.2KB
Zip 2.35x +48B +2
ZipWith 3.15x +48B +2
Unique +24B +1
UniqueBy +48B +2
Union +72B +3
Intersect +72B +3
Difference 2.33x -26.7KB -29
GroupBySlice +24B +1
CountBy +24B +1
CountByValue +24B +1
ToMap -24B

Pipelines

Op Speed vs lo Memory Allocs
Pipeline F→M→T→R 1.79x -12.2KB

Mutating ops

Op Speed vs lo Memory Allocs
Map 2.21x -8.2KB
Filter 1.41x -8.2KB
Reverse +24B +1
Shuffle 1.57x +24B +1

How to read the benchmarks

  • means the two libraries are effectively equivalent
  • Explicit memory deltas show fixed wrapper overhead vs avoided allocations
  • 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.

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) → Map (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.

This library borrows slices by default. It does not perform defensive copies. Use Clone() or ItemsCopy() to explicitly copy.

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
  • 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
  • Not designed to hide allocation, mutation, or ordering semantics

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 and returns a derived value
  • immutable - returns a new collection; the original is unchanged
  • mutable - modifies the collection in place and returns the same instance
  • terminal - ends the fluent pipeline and returns a non-collection result

These annotations describe observable behavior, not implementation details.

Terminal operations do not return a Collection and cannot be chained further. They are designed to be allocation-free under New() where possible.

Allocation and copying are explicitly documented per method. Some readonly or immutable operations may allocate internally when required (e.g. grouping, chunking, scratch copies), but never mutate the receiver.

Borrowed slices, in-place mutation, and view semantics are intentional and visible.

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 ItemsCopy
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 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 Prepend Tap Times Transform Zip ZipWith

Access

Items · readonly · terminal

Items returns the backing 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
//   }
// ]

ItemsCopy · readonly · terminal

ItemsCopy returns a copy of the collection's items.

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

Aggregation

Avg · readonly · terminal

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 · terminal

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 · terminal

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 · terminal

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[main.Point]int {
//  {1 1} => 2 #int
//  {2 2} => 1 #int
// }

Max · readonly · terminal

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 · terminal

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 · terminal

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 · terminal

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 · terminal

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 · terminal

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)
// []int(nil)

Reduce · readonly · terminal

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 · terminal

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 · immutable · chainable

Clone returns a copy of the collection.

Example: basic cloning

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

clone = clone.Append(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 · chainable

New creates a new Collection from the provided slice and borrows it.

NewNumeric · immutable · chainable

NewNumeric wraps a slice of numeric types in a NumericCollection and borrows it.

Debugging

Dd · terminal

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 · chainable

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 · terminal

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 · terminal

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 · terminal

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 · chainable

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)
c.Sort(func(a, b collection.Pair[string, int]) bool {
	return a.Key < b.Key
})
collection.Dump(c.Items())

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

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
	}).
	Sort(func(a, b collection.Pair[string, Config]) bool {
		return a.Key < b.Key
	}).
	Items()

collection.Dump(out)

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

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 #int
//   bob => 2 #int
// }

ToMap · readonly · terminal

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)
// #map[string]int {
//  alice => 5 #int
//  bob => 3 #int
//  carol => 5 #int
// }

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)
// #map[int]main.User {
//  1 => #main.User {
//    +ID   => 1 #int
//    +Name => "Alice" #string
//  }
//  2 => #main.User {
//    +ID   => 2 #int
//    +Name => "Bob" #string
//  }
// }

ToMapKV · readonly · terminal

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 #int
//  b => 2 #int
//  c => 3 #int
// }

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]main.Config {
//  router-1 => #main.Config {
//    +Enabled => true #bool
//    +Timeout => 30 #int
//  }
//  router-3 => #main.Config {
//    +Enabled => true #bool
//    +Timeout => 45 #int
//  }
// }

Ordering

After · immutable · chainable

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 · chainable

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())
// #[]main.User [
//  0 => #main.User {
//    +Name  => "Alice" #string
//    +Admin => false #bool
//  }
//  1 => #main.User {
//    +Name  => "Bob" #string
//    +Admin => false #bool
//  }
// ]

Reverse · mutable · chainable

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())
// #[]main.User [
//   0 => #main.User {
//     +ID => 3 #int
//   }
//   1 => #main.User {
//     +ID => 2 #int
//   }
//   2 => #main.User {
//     +ID => 1 #int
//   }
// ]

Shuffle · mutable · chainable

Shuffle shuffles the collection in place and returns the same collection.

Example: integers

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

Example: strings – chaining

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

collection.Dump(out2)

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 · chainable

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 · terminal

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 · terminal

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 · terminal

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 #int
// true #bool

Example: out of bounds

v2, ok2 := c.At(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"},
})

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

Contains · readonly · terminal

Contains returns true if the collection contains the given value.

Example: integers

c := collection.New([]int{1, 2, 3, 4, 5})
hasTwo := collection.Contains(c, 2)
collection.Dump(hasTwo)
// true #bool

Example: strings

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

First · readonly · terminal

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 · terminal

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 · terminal

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 #int
// true #bool

Example: not found

idx2, ok2 := c.IndexWhere(func(v int) bool { return v == 99 })
collection.Dump(idx2, 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: "Carol"},
})

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

collection.Dump(idx3, ok3)
// 1 #int
// true #bool

IsEmpty · readonly · terminal

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 · terminal

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 · terminal

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 · terminal

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 · terminal

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 · terminal

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 · chainable

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 · chainable

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 · chainable

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 · chainable

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 · chainable

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())
// #[]main.User [
//  0 => #main.User {
//    +ID   => 1 #int
//    +Name => "Alice" #string
//  }
//  1 => #main.User {
//    +ID   => 2 #int
//    +Name => "Bob" #string
//  }
// ]

UniqueBy · immutable · chainable

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())
// #[]main.User [
//  0 => #main.User {
//    +ID   => 1 #int
//    +Name => "Alice" #string
//  }
//  1 => #main.User {
//    +ID   => 2 #int
//    +Name => "Bob" #string
//  }
// ]

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 · chainable

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 · terminal

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 · chainable

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 · terminal

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 · terminal

Pop removes and returns the last item in the collection.

Example: integers

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

Example: strings

c2 := collection.New([]string{"a", "b", "c"})
item2, ok2 := c2.Pop()
collection.Dump(item2, ok2, c2.Items())
// "c" #string
// true #bool
// #[]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, ok3 := users.Pop()
collection.Dump(item3, ok3, users.Items())
// #main.User {
//   +ID   => 2 #int
//   +Name => "Bob" #string
// }
// true #bool
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
// ]

Example: empty collection

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

PopN · mutable · terminal

PopN removes and returns the last n items in original order.

Example: integers – pop 2

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

Example: strings – pop 1

c2 := collection.New([]string{"a", "b", "c"})
popped2 := c2.PopN(1)
collection.Dump(popped2, c2.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 := users.PopN(2)
collection.Dump(popped3, users.Items())
// #[]main.User [
//   0 => #main.User {
//     +ID   => 2 #int
//     +Name => "Bob" #string
//   }
//   1 => #main.User {
//     +ID   => 3 #int
//     +Name => "Carol" #string
//   }
// ]
// #[]main.User [
//   0 => #main.User {
//     +ID   => 1 #int
//     +Name => "Alice" #string
//   }
// ]

Example: integers - n <= 0 → returns nil, no change

c3 := collection.New([]int{1, 2, 3})
popped4 := c3.PopN(0)
collection.Dump(popped4, c3.Items())
// []int(nil)
// #[]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 := c4.PopN(10)
collection.Dump(popped5, c4.Items())
// #[]string [
//   0 => "x" #string
//   1 => "y" #string
// ]
// #[]string [
// ]

Skip · immutable · chainable

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 · chainable

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())
// #[]main.User [
//  0 => #main.User {
//    +ID => 1 #int
//  }
//  1 => #main.User {
//    +ID => 2 #int
//  }
// ]

Take · immutable · chainable

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 · chainable

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())
// #[]main.User [
//  0 => #main.User {
//    +ID => 3 #int
//  }
// ]

TakeUntil · immutable · chainable

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 · chainable

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 · chainable

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 · chainable

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 · chainable

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 · readonly · chainable

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 · mutable · chainable

Map applies a same-type transformation in place and returns the same 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 · chainable

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 · immutable · chainable

Merge merges the given data into a new 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 · immutable · chainable

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 · terminal

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 := collection.Pipe(c, func(col *collection.Collection[int]) int {
	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 := collection.Pipe(c2, func(col *collection.Collection[string]) string {
	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 := collection.Pipe(users, func(col *collection.Collection[User]) []string {
	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
// ]

Prepend · mutable · chainable

Prepend adds the given values to the beginning of the collection.

Example: integers

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

Example: strings

letters := collection.New([]string{"c", "d"})
letters.Prepend("a", "b")
collection.Dump(letters.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"},
})

users.Prepend(User{ID: 1, Name: "Alice"})
collection.Dump(users.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{})
empty.Prepend(9, 8)
collection.Dump(empty.Items())
// #[]int [
//   0 => 9 #int
//   1 => 8 #int
// ]

Example: integers - Prepending no values → no change

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

Tap · immutable · chainable

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)
// #[]int [
//  0 => 2 #int
//  1 => 3 #int
// ]
// #[]int [
//  0 => 1 #int
//  1 => 2 #int
//  2 => 3 #int
// ]

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())
		// #[]int [
		//  0 => 10 #int
		//  1 => 20 #int
		//  2 => 30 #int
		// ]
	}).
	Filter(func(v int) bool { return v > 10 })

collection.Dump(c2.Items()) // ensures c2 is used
// #[]int [
//  0 => 20 #int
//  1 => 30 #int
// ]

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())
	// #[]main.User [
	//  0 => #main.User {
	//    +ID   => 1 #int
	//    +Name => "Alice" #string
	//  }
	//  1 => #main.User {
	//    +ID   => 2 #int
	//    +Name => "Bob" #string
	//  }
	// ]
})

collection.Dump(users2.Items()) // ensures users2 is used
// #[]main.User [
//  0 => #main.User {
//    +ID   => 1 #int
//    +Name => "Alice" #string
//  }
//  1 => #main.User {
//    +ID   => 2 #int
//    +Name => "Bob" #string
//  }
// ]

Times · immutable · chainable

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 · terminal

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())
// #[]main.User [
//  0 => #main.User {
//    +ID   => 1 #int
//    +Name => "ALICE" #string
//  }
//  1 => #main.User {
//    +ID   => 2 #int
//    +Name => "BOB" #string
//  }
// ]

Zip · immutable · chainable

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·1,string] [
//   0 => #collection.Tuple[main.User·1,string] {
//     +First  => #main.User {
//       +ID   => 1 #int
//       +Name => "Alice" #string
//     }
//     +Second => "admin" #string
//   }
//   1 => #collection.Tuple[main.User·1,string] {
//     +First  => #main.User {
//       +ID   => 2 #int
//       +Name => "Bob" #string
//     }
//     +Second => "user" #string
//   }
// ]

ZipWith · immutable · chainable

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