Skip to content

Latest commit

 

History

216 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

goforj/collection logo

Fluent collections for Go. Iterate, filter, transform, sort, reduce, group, and debug data with a tiny dependency footprint.

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

Features

  • Fluent chaining - pipeline your operations like Laravel Collections
  • Fully generic (Slice[T]) - the public collection API uses typed generic signatures; debugging helpers accept arbitrary values for inspection
  • Tiny dependency footprint - only godump for debugging helpers
  • Go-native slice access - use len, indexing, range, and ordinary slice conversions directly
  • Explicit ownership behavior - slice views, independent results, and in-place mutations are documented per operation
  • Map / Filter / Reduce - clean functional transforms
  • Generic methods - type-changing transforms remain fluent on Go 1.27+
  • First / Last / FirstWhere / IndexWhere helpers
  • Sort, GroupBy, Chunk, and more
  • Borrow-by-default - no defensive copies unless you ask for them
  • Standard-library interop - use slices, iter, and encoding/json directly
  • Developer-friendly debug helpers (Dump(), Dd(), DumpStr())
  • Works with any Go type, including structs, pointers, and deeply nested composites

Fluent Chaining

Many methods return a Slice, allowing fluent method chaining without a wrapper object.

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},
    {Device: "router-4", Region: "us-west", Errors: 9},
    {Device: "router-5", Region: "eu-west", Errors: 7},
}

// Clone creates a top-level ownership boundary before the mutable stages.
collection.
    New(events). // Construction
    Clone(). // Construction
    Retain(func(e DeviceEvent) bool { return e.Errors > 5 }). // Mutation
    Sort(func(a, b DeviceEvent) bool { return a.Errors > b.Errors }). // Ordering
    Take(3). // Slicing
    TakeUntil(func(e DeviceEvent) bool { return e.Errors <= 9 }). // Slicing (stop when predicate becomes true)
    Reverse(). // Ordering
    Dump() // Debugging

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

Go 1.27 generic methods keep type-changing pipelines fluent:

regionsByPrefix := collection.New(events).
    Map(func(event DeviceEvent) string { return event.Region }).
    UniqueBy(func(region string) string { return region }).
    GroupBy(func(region string) string { return region[:2] })
fmt.Println(len(regionsByPrefix))
// 2

Performance Benchmarks

Equivalent operations are benchmarked against lo, with ownership and API differences labeled separately. Both the summary and full results use collection.New, which borrows its input.

Benchmark tables and methodology

lo is a major inspiration for this project.

Both libraries provide generic operations over ordinary slices. The main difference is API shape: collection.Slice adds receiver methods for eager fluent chains, while lo primarily uses free functions and provides mutable and iterator variants in separate packages. The tables label allocation, view, mutation, API, and different-work cases rather than treating them as equivalent.

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

Matched v2/v4 regression benchmarks for mutating, copied, and pipeline workloads live in ./docs/regression; its go.mod records the exact v2 baseline.

Full raw tables: see BENCHMARKS.md.

Read-only scalar ops

Op Speed vs lo Memory Allocs
All
Any
None
First below floor
Last below floor
FirstWhere same loop
IndexWhere
slices.Contains
Reduce (sum)
Sum
Min
Max
Each

Transforming ops

Op Speed vs lo Memory Allocs
Chunk view trade-off ownership trade-off ownership trade-off
Filter
Map
Take below floor
Skip view trade-off ownership trade-off ownership trade-off
SkipLast view trade-off ownership trade-off ownership trade-off
Zip inconclusive
ZipWith 2.9x faster
UniqueComparable
UniqueBy
Union inconclusive
Intersect
Difference different work API trade-off API trade-off
GroupBy
CountBy
CountByValue
ToMap

Pipelines

Op Speed vs lo Memory Allocs
Pipeline F→M→T→R

Mutating ops

Op Speed vs lo Memory Allocs
Retain 1.6x slower
Reverse 1.1x faster
Shuffle 3.7x faster
Transform

How to read the benchmarks

  • In Speed/Timing, means the median is inside ±10% (±15% in the condensed read-only scalar table)
  • below floor means both timings are under 50 ns, so no relative conclusion is drawn
  • inconclusive means the median is outside that band but paired samples did not consistently establish the difference
  • Nx faster/slower is calculated from the measured, unrounded medians and appears only when all paired samples establish the same direction; exact values remain machine- and build-specific
  • In Memory/Allocs, means both implementations produced the same measured result
  • same loop means both implementations compile to the same machine loop, so binary-placement skew is not presented as a library difference
  • Explicit memory deltas show allocation differences for equivalent work; ownership and API trade-offs are labeled separately
  • Single-operation helpers are expected to be close when they perform equivalent work
  • Multi-step pipelines show the cost of the selected ownership model

Allocation and mutation are explicit

Version 3 has one Go-native, slice-backed representation. Every Slice supports len, indexing, slicing, and range; each operation documents how it treats the backing array.

  • Map, Filter, Concat, and Prepend return independent results
  • Retain and Transform mutate in place; the shortened result from Retain must be captured
  • Sort, Reverse, and Shuffle mutate elements in place
  • View-producing slicing operations return capacity-capped views
  • Clone creates an intentional ownership boundary before mutation

The benchmark tables compare equivalent pure operations separately from Retain and Transform. Chunk, Skip, and SkipLast are ownership trade-offs because collection returns capacity-capped views while lo returns copied slices.

Explicit branching with Clone

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

New borrows slices by default. Use Clone() before an in-place operation when the original slice's element slots or order must remain unchanged.

When you want to branch a pipeline or preserve the original data, Clone() creates a shallow copy of the top-level slice. Subsequent operations that replace, reorder, or overwrite collection entries are isolated; Clone() does not deep-copy element values or values they reference.

events := collection.New(deviceEvents)

// In-place alert filtering with a bounded result
alerts := events.
    Clone().
    Retain(func(e DeviceEvent) bool { return e.Severity >= Critical }).
    Take(10)

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

This makes divergence points explicit and intentional.

Copying and mutation behavior is documented per operation.

Design Principles

  • Type-safe: the collection API uses typed generic signatures; debugging helpers are the exception for arbitrary-value inspection
  • 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 a collection; its initial order is unspecified, so sort when a particular order matters
  • ToMap reduces collections back into maps explicitly

This makes map materialization visible and leaves deterministic ordering to an explicit Sort.

Behavior semantics

Each exported function and method declares how it interacts with the collection:

  • readonly - does not directly mutate the receiver's backing array; callbacks and debugging helpers can still have side effects
  • immutable - returns a value without mutating the receiver; individual method docs state whether it allocates or returns a view
  • mutable - may modify elements in the receiver's backing array
  • 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 Slice and cannot be chained further. They are designed to be allocation-free under New() where possible.

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

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

Native slice interoperability

Slice[T] is a named slice, so ordinary Go operations work directly:

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

fmt.Println(len(values))
// 3
fmt.Println(values[1])
// 20

for _, value := range values {
    fmt.Println(value)
}
// 10
// 20
// 30

Use slices.Values(values) or slices.All(values) when an iterator is useful; no collection-specific lazy wrapper is required.

Runnable examples

Every exported function and method has a corresponding runnable example under ./examples.

The checked-in examples are generated from GoDoc comments when the documentation is refreshed; they are intended to stay aligned with the README and GoDoc.

Automated checks build and execute the checked-in examples, then compare their output with the documentation.

This helps catch example regressions as the API evolves.

Installation

This package requires Go 1.27 or newer. Consumers that cannot upgrade their toolchain can remain on v2.

Existing users should read Migrating to v4 for the complete API and ownership changes.

go get github.com/goforj/collection/v4

API Index

Group Functions and methods
Aggregation Avg · CountBy · CountByValue · Max · MaxBy · Median · Min · MinBy · Mode · Reduce · Sum
Construction Clone · New
Debugging Dd · Dump · DumpStr · Slice.Dump
Grouping GroupBy
Maps FromMap · ToMap
Ordering After · Reverse · Shuffle · Sort
Querying All · Any · At · First · FirstWhere · IndexWhere · Last · LastWhere · None
Set Operations Difference · Intersect · SymmetricDifference · Union · Unique · UniqueBy · UniqueComparable
Slicing Chunk · Filter · Partition · Retain · Skip · SkipLast · Take · TakeLast · TakeUntil · Window
Transformation Concat · Each · Map · Multiply · Prepend · Tap · Times · Transform · Zip · ZipWith

Aggregation

Avg - readonly - terminal

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

Example: integers

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

Example: float

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

CountBy - readonly - terminal

CountBy returns occurrence counts keyed by the extracted value.

numbers := collection.New([]int{1, 2, 3, 5})
counts := numbers.CountBy(func(number int) string {
	if number%2 == 0 {
		return "even"
	}
	return "odd"
})
collection.Dump(counts)
// #map[string]int {
//   even => 1 #int
//   odd => 3 #int
// }

CountByValue - readonly - terminal

CountByValue returns the number of occurrences of each distinct item in c.

T must be comparable.

collection.Dump(collection.CountByValue([]string{"go", "forj", "go"}))
// #map[string]int {
//   forj => 1 #int
//   go => 2 #int
// }

Max - readonly - terminal

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

Example: integers

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

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

Example: floats

values2 := []float64{1.5, 9.2, 4.4}

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

Example: empty numeric slice

empty := []int{}

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

MaxBy - readonly - terminal

MaxBy returns the item whose extracted key is the largest.

words := collection.New([]string{"pear", "fig", "banana"})
longest, ok := words.MaxBy(func(word string) int {
	return len(word)
})
collection.Dump(longest, ok)
// "banana" #string
// true #bool

Median - readonly - terminal

Median returns the statistical median of a numeric slice as float64. It returns (0, false) if the slice is empty. Median copies the input before sorting, so it allocates O(n) storage and does not mutate the input slice.

  • Odd count: middle value.
  • Even count: average of the two middle values.

Example: integers - odd number of items

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

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

Example: integers - even number of items

values2 := []int{10, 2, 4, 6}

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

Example: floats

values3 := []float64{1.1, 9.9, 3.3}

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

Example: integers - empty numeric slice

empty := []int{}

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

Min - readonly - terminal

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

Example: integers

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

Example: floats

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

Example: integers - empty collection

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

MinBy - readonly - terminal

MinBy returns the item whose extracted key is the smallest.

words := collection.New([]string{"pear", "fig", "banana"})
shortest, ok := words.MinBy(func(word string) int {
	return len(word)
})
collection.Dump(shortest, ok)
// "fig" #string
// true #bool

Mode - readonly - terminal

Mode returns the most frequent numeric value or values in a slice. If multiple values tie for highest frequency, all are returned in first-seen order.

Example: integers - single mode

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

Example: integers - tie for mode

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

Example: floats

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

Example: integers - empty collection

collection.Dump(collection.Mode([]int{}))
// []int(nil)

Reduce - readonly - terminal

Reduce collapses the collection into a single accumulated value. The accumulator may have a different type R from the collection's elements.

This is useful for computing sums, concatenations, aggregates, or any fold-style reduction.

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 items in a numeric slice. If the slice is empty, Sum returns the zero value of T.

Example: integers

collection.Dump(collection.Sum([]int{1, 2, 3}))
// 6 #int

Example: floats

collection.Dump(collection.Sum([]float64{1.5, 2.5}))
// 4.000000 #float64

Example: integers - empty collection

collection.Dump(collection.Sum([]int{}))
// 0 #int

Construction

Clone - immutable - chainable

Clone returns a copy of the collection.

The returned collection has its own backing slice, so element assignments and slice operations on the clone do not affect the original collection. Clone is shallow: pointers, maps, slices, and other references stored in elements remain shared.

Clone is intended to be used when branching a pipeline while preserving the original collection.

Example: basic cloning

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

clone.Transform(func(value int) int { return value * 10 })

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

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

Example: branching pipelines

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

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

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

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

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

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

New - immutable - chainable

New creates a Slice from items and borrows their backing array.

values := collection.New([]int{10, 20, 30})
fmt.Println(len(values))
// 3
fmt.Println(values[1])
// 20

total := 0
for _, value := range values {
	total += value
}
fmt.Println(total)
// 60

Debugging

Dd - readonly - 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.

This method never returns.

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

Dump - readonly - terminal

Dump is a convenience function that calls godump.Dump.

collection.Dump(collection.New([]int{1, 2, 3}))
// #[]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.

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

Slice.Dump - readonly - chainable

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

Example: integers

collection.New([]int{1, 2, 3}).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
// ]

Grouping

GroupBy - readonly - terminal

GroupBy partitions this Slice into independent built-in slices keyed by the extracted value.

numbers := collection.New([]int{1, 2, 3, 4})
groups := numbers.GroupBy(func(number int) string {
	if number%2 == 0 {
		return "even"
	}
	return "odd"
})
collection.Dump(groups["even"], groups["odd"])
// #[]int [
//   0 => 2 #int
//   1 => 4 #int
// ]
// #[]int [
//   0 => 1 #int
//   1 => 3 #int
// ]
fmt.Println(len(groups["even"]))
// 2
fmt.Println(groups["odd"][0])
// 1
collection.Dump(groups["even"][:1])
// #[]int [
//   0 => 2 #int
// ]

Maps

FromMap - immutable - chainable

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

The iteration order of the resulting collection is unspecified, matching Go's map iteration semantics.

This function does not mutate the input map.

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.First < b.First
})
collection.Dump(c)
// #[]collection.Pair[string,int] [
//   0 => #collection.Pair[string,int] {
//     +First  => "a" #string
//     +Second => 1 #int
//   }
//   1 => #collection.Pair[string,int] {
//     +First  => "b" #string
//     +Second => 2 #int
//   }
//   2 => #collection.Pair[string,int] {
//     +First  => "c" #string
//     +Second => 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.Second.Enabled
	}).
	Sort(func(a, b collection.Pair[string, Config]) bool {
		return a.First < b.First
	})

collection.Dump(out)
// #[]collection.Pair[string,main.Config·1] [
//   0 => #collection.Pair[string,main.Config·1] {
//     +First     => "router-1" #string
//     +Second    => #main.Config {
//       +Enabled => true #bool
//       +Timeout => 30 #int
//     }
//   }
//   1 => #collection.Pair[string,main.Config·1] {
//     +First     => "router-3" #string
//     +Second    => #main.Config {
//       +Enabled => true #bool
//       +Timeout => 45 #int
//     }
//   }
// ]

ToMap - readonly - terminal

ToMap reduces this collection into a map using the provided key and value functions. If multiple items produce the same key, the value derived from the last item wins.

words := collection.New([]string{"go", "forj"})
lengths := words.ToMap(
	func(word string) string { return word },
	func(word string) int { return len(word) },
)
collection.Dump(lengths)
// #map[string]int {
//   forj => 4 #int
//   go => 2 #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.

NOTE: returns a view (shares backing array). Use Clone() to detach.

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

Reverse - mutable - chainable

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

This operation performs no allocations.

Example: integers

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

Example: strings - chaining

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

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)
// #[]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.

This operation mutates the receiver's backing slice.

Example: integers

c := collection.New([]int{1, 2, 3, 4, 5})
c.Shuffle()
fmt.Println(len(c), collection.Sum(c))
// 5 15

Example: strings - chaining

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

fmt.Println(len(out2))
// 4

Example: structs

type User struct {
	ID int
}

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

users.Shuffle()
fmt.Println(len(users))
// 4

Sort - mutable - chainable

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

The comparison function less(a, b) should return true if a should come before b in the sorted order.

This operation mutates the underlying slice and does not allocate a new element backing slice. The underlying sort implementation may make small internal allocations.

Example: integers

c := collection.New([]int{5, 1, 4, 2})
c.Sort(func(a, b int) bool { return a < b })
collection.Dump(c)
// #[]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)
// #[]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)
// #[]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

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

Example: integers - not all even

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

Example: strings - all non-empty

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

Example: empty collection (vacuously true)

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

Any - readonly - terminal

Any returns true if at least one item satisfies fn.

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

At - readonly - terminal

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

This method is safe and does not panic for out-of-range indices.

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

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.

This method is equivalent to Laravel's collection->first(fn) and mirrors the behavior found in functional collections in other languages.

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).

This operation performs no allocations and short-circuits on the first match.

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

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

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

Example: integers - some even

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

Example: empty collection

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

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})

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

Example: strings

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

collection.Dump(collection.Difference(left, right))
// #[]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"},
})

collection.Dump(collection.Difference(groupA, groupB))
// #[]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.

Order follows the second collection. Duplicates are preserved based on the second collection.

Example: integers

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

collection.Dump(collection.Intersect(a, b))
// #[]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"})

collection.Dump(collection.Intersect(left, right))
// #[]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"},
})

collection.Dump(collection.Intersect(groupA, groupB))
// #[]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})

collection.Dump(collection.SymmetricDifference(a, b))
// #[]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"})

collection.Dump(collection.SymmetricDifference(left, right))
// #[]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"},
})

collection.Dump(collection.SymmetricDifference(groupA, groupB))
// #[]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})

collection.Dump(collection.Union(a, b))
// #[]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"})

collection.Dump(collection.Union(left, right))
// #[]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"},
})

collection.Dump(collection.Union(groupA, groupB))
// #[]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.

The eq function should return true when two values are considered equal.

Example: integers

c1 := collection.New([]int{1, 2, 2, 3, 4, 4, 5})
collection.Dump(c1.Unique(func(a, b int) bool { return a == b }))
// #[]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)
// #[]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)
// #[]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 collection containing the first item for each extracted key.

words := collection.New([]string{"go", "up", "forj", "code"})
unique := words.UniqueBy(func(word string) int {
	return len(word)
})
collection.Dump(unique)
// #[]string [
//   0 => "go" #string
//   1 => "forj" #string
// ]

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. It uses a map to track seen values, so it has expected linear time and allocates storage for both the map and the result.

Example: integers

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

Example: strings

collection.Dump(collection.UniqueComparable([]string{"A", "a", "B", "B"}))
// #[]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.

If size <= 0, nil is returned.

Chunk allocates the outer result slice. Each chunk is a capacity-capped view that shares the backing array with the source collection.

Example: integers

collection.Dump(collection.New([]int{1, 2, 3, 4, 5}).Chunk(2))
// #[][]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)
// #[][]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 - immutable - chainable

Filter keeps only the elements for which fn returns true.

Filter allocates a new Slice and leaves c and its backing storage unchanged.

Example: integers

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

Example: strings

c2 := collection.New([]string{"apple", "banana", "cherry", "avocado"})
c2 = c2.Filter(func(v string) bool {
	return strings.HasPrefix(v, "a")
})
collection.Dump(c2)
// #[]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 = users.Filter(func(u User) bool {
	return strings.HasPrefix(u.Name, "A")
})

collection.Dump(users)
// #[]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 slices based on predicate fn. The first slice 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, odds)
// #[]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, other)
// #[]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, inactive)
// #[]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
//   }
// ]

Retain - mutable - chainable

Retain keeps items for which fn returns true in c's existing backing array.

Retain returns a capacity-capped, shortened slice header, so callers should retain its result when subsequent operations must observe the new length.

values := collection.New([]int{1, 2, 3, 4})
evens := values.Retain(func(value int) bool { return value%2 == 0 })
collection.Dump(evens)
// #[]int [
//   0 => 2 #int
//   1 => 4 #int
// ]
fmt.Println(values)
// [2 4 0 0]

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.

This operation performs no element allocations; it re-slices the underlying slice.

NOTE: returns a view (shares backing array). Use Clone() to detach.

Example: integers

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

Example: skip none

out2 := c.Skip(0)
collection.Dump(out2)
// #[]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)
// #[]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)
// #[]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.

This operation performs no element allocations; it re-slices the underlying slice.

NOTE: returns a view (shares backing array). Use Clone() to detach.

Example: integers

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

Example: skip none

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

Take - immutable - chainable

Take returns a capacity-capped view containing the first n items.

If n exceeds the collection length, the entire collection is returned. If n == 0, an empty collection is returned.

NOTE: returns a view (shares backing array). Use Clone() to detach.

Example: integers - take first 3

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

Example: integers - n exceeds length → whole collection

c3 := collection.New([]int{10, 20})
out3 := c3.Take(10)
collection.Dump(out3)
// #[]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)
// #[]int [
// ]

TakeLast - immutable - chainable

TakeLast returns a capacity-capped view 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.

This operation performs no element allocations; it re-slices the underlying slice.

NOTE: returns a view (shares backing array). Use Clone() to detach.

Example: integers

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

Example: take none

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

Example: take all

out3 := c.TakeLast(10)
collection.Dump(out3)
// #[]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)
// #[]main.User [
//  0 => #main.User {
//    +ID => 3 #int
//  }
// ]

TakeUntil - immutable - chainable

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

NOTE: returns a view (shares backing array). Use Clone() to detach.

Example: integers - stop when value >= 3

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

Example: integers - predicate immediately true → empty result

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

Example: integers - no match → full list returned

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

Window - readonly - terminal

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.

Window allocates the outer result slice. Each window is a capacity-capped view that shares the backing array with the source collection.

Example: integers - step 1

collection.Dump(collection.New([]int{1, 2, 3, 4, 5}).Window(3, 1))
// #[][]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

collection.Dump(collection.New([]string{"a", "b", "c", "d", "e"}).Window(2, 2))
// #[][]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 := points.Window(2, 1)
collection.Dump(win3)
// #[][]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

Concat - immutable - chainable

Concat returns an independent collection containing c followed by values.

Callers must capture the returned Slice because a value receiver cannot extend c's slice header. The returned collection never shares backing storage with c.

Example: strings

c := collection.New([]string{"John Doe"})
concatenated := c.
	Concat([]string{"Jane Doe"}).
	Concat([]string{"Johnny Doe"})
collection.Dump(concatenated)
// #[]string [
//  0 => "John Doe" #string
//  1 => "Jane Doe" #string
//  2 => "Johnny Doe" #string
// ]

Example: spare capacity

backing := make([]int, 2, 4)
copy(backing, []int{1, 2})
values := collection.New(backing)
values = values.Concat([]int{3, 4})
fmt.Println(values)
// [1 2 3 4]

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

Map maps this Slice to a newly allocated Slice with a potentially different element type.

numbers := collection.New([]int{1, 2, 3, 4})
labels := numbers.Map(func(number int) string {
	if number%2 == 0 {
		return "even"
	}
	return "odd"
})
collection.Dump(labels)
// #[]string [
//   0 => "odd" #string
//   1 => "even" #string
//   2 => "odd" #string
//   3 => "even" #string
// ]
fmt.Println(numbers[0])
// 1

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})
collection.Dump(ints.Multiply(3))
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 1 #int
//   3 => 2 #int
//   4 => 1 #int
//   5 => 2 #int
// ]

Example: strings

collection.Dump(collection.New([]string{"a", "b"}).Multiply(2))
// #[]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"}})
collection.Dump(users.Multiply(2))
// #[]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

collection.Dump(ints.Multiply(0))
// #[]int [
// ]

Prepend - immutable - chainable

Prepend returns an independently backed Slice containing values followed by c.

It allocates exactly enough storage for the result and leaves c unchanged.

Example: integers

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

Example: strings

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

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

Example: integers - Prepending no values → no change

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

Tap - mutable - chainable

Tap invokes fn with the Slice value for side effects such as logging, debugging, or inspection, then returns the Slice to allow chaining.

The callback receives a borrowed Slice and may mutate its elements. Use Clone before Tap when the original backing array must remain isolated. The slice header is passed by value, so reslicing, appending, or assigning a shortened Slice inside fn does not change the header returned by Tap.

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.Slice[int]) {
		captured1 = append([]int(nil), col...) // 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)
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.Slice[int]) {
		collection.Dump(col)
		// #[]int [
		//  0 => 10 #int
		//  1 => 20 #int
		//  2 => 30 #int
		// ]
	}).
	Filter(func(v int) bool { return v > 10 })

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

collection.Dump(users2) // 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.

If count <= 0, an empty collection is returned.

Example: integers - double each index

cTimes1 := collection.Times(5, func(i int) int {
	return i * 2
})
collection.Dump(cTimes1)
// #[]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)
// #[]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)
// #[]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 - chainable

Transform applies a same-type transformation in place and returns the same collection.

Transform mutates the receiver's backing slice. Use Clone() if you need isolation.

Example: integers

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

c.Transform(func(v int) int {
	return v * 10
})

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

Example: strings

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

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

collection.Dump(upper)
// #[]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.Transform(func(u User) User {
	u.Name = strings.ToUpper(u.Name)
	return u
})

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

Zip - immutable - terminal

Zip combines this collection with values element-wise into pairs. The resulting length is the smaller of the two inputs.

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

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

ZipWith - immutable - chainable

ZipWith combines this collection with a slice using fn up to the shorter length.

left := collection.New([]int{1, 2, 3})
right := collection.New([]int{10, 20})
sums := left.ZipWith(right, func(a, b int) int {
	return a + b
})
collection.Dump(sums)
// #[]int [
//   0 => 11 #int
//   1 => 22 #int
// ]

Development

Use make test for the root module, make vet for static checks, and make generate to refresh the generated README API reference. The docs and examples directories are separate Go modules and can be tested from their own directories when changed.

About

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

Topics

Resources

Stars

32 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages