From ec27cc81533218348be5583fb802ab411e8b9bd0 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 19:37:28 -0600 Subject: [PATCH 01/30] initial v2 commit --- README.md | 460 +++++++++------------------------ after.go | 8 +- append.go | 54 +--- append_test.go | 124 ++++----- before.go | 6 +- chunk.go | 2 + clone.go | 8 +- collection.go | 101 +++++++- collection_test.go | 4 +- constructor_test.go | 89 +++++++ contains.go | 38 +-- contains_test.go | 10 +- difference.go | 4 +- docs/bench/main.go | 2 +- each.go | 2 +- each_test.go | 11 +- examples/attach/main.go | 22 ++ examples/attachnumeric/main.go | 22 ++ examples/clone/main.go | 4 +- examples/contains/main.go | 30 +-- examples/findwhere/main.go | 61 ----- examples/items/main.go | 2 +- examples/itemscopy/main.go | 20 ++ examples/merge/main.go | 2 +- examples/pipe/main.go | 6 +- examples/pluck/main.go | 61 ----- examples/pop/main.go | 23 +- examples/popn/main.go | 44 ++-- examples/prepend/main.go | 25 +- examples/push/main.go | 53 ---- examples/shuffle/main.go | 15 +- examples/take/main.go | 2 + examples/takelast/main.go | 2 + examples/where/main.go | 51 ---- find_where.go | 64 ----- find_where_test.go | 96 ------- from_map.go | 2 +- intersect.go | 4 +- map.go | 7 +- map_test.go | 8 + map_to_test.go | 72 ++++++ merge.go | 8 +- merge_test.go | 4 + multiply.go | 6 +- partition.go | 4 +- pipe.go | 11 +- pipe_test.go | 18 +- pluck.go | 64 +---- pluck_test.go | 139 ---------- pop.go | 108 ++++---- pop_test.go | 78 +++--- prepend.go | 30 +-- prepend_test.go | 48 ++-- reverse_test.go | 2 +- shuffle.go | 29 ++- shuffle_test.go | 37 +-- skip.go | 8 +- skip_last.go | 8 +- skip_last_test.go | 2 +- skip_test.go | 2 +- symmetric_difference.go | 2 +- take.go | 12 +- take_last.go | 8 +- take_last_test.go | 2 +- take_until.go | 22 +- times.go | 4 +- union.go | 2 +- unique.go | 2 +- unique_by.go | 4 +- unique_comparable.go | 4 +- where.go | 52 ---- where_test.go | 24 -- window.go | 12 +- zip.go | 4 +- 74 files changed, 862 insertions(+), 1519 deletions(-) create mode 100644 constructor_test.go create mode 100644 examples/attach/main.go create mode 100644 examples/attachnumeric/main.go delete mode 100644 examples/findwhere/main.go create mode 100644 examples/itemscopy/main.go delete mode 100644 examples/pluck/main.go delete mode 100644 examples/push/main.go delete mode 100644 examples/where/main.go delete mode 100644 find_where.go delete mode 100644 find_where_test.go create mode 100644 map_to_test.go delete mode 100644 pluck_test.go delete mode 100644 where.go delete mode 100644 where_test.go diff --git a/README.md b/README.md index 36af4d3..836fa78 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Latest tag - Tests + Tests Go Report Card

@@ -259,25 +259,25 @@ go get github.com/goforj/collection | Group | Functions | |------:|-----------| -| **Access** | [Items](#items) | +| **Access** | [Items](#items) [ItemsCopy](#itemscopy) | | **Aggregation** | [Avg](#avg) [Count](#count) [CountBy](#countby) [CountByValue](#countbyvalue) [Max](#max) [MaxBy](#maxby) [Median](#median) [Min](#min) [MinBy](#minby) [Mode](#mode) [Reduce](#reduce) [Sum](#sum) | -| **Construction** | [Clone](#clone) [New](#new) [NewNumeric](#newnumeric) | +| **Construction** | [Attach](#attach) [AttachNumeric](#attachnumeric) [Clone](#clone) [New](#new) [NewNumeric](#newnumeric) | | **Debugging** | [Dd](#dd) [Dump](#dump) [DumpStr](#dumpstr) | | **Grouping** | [GroupBy](#groupby) [GroupBySlice](#groupbyslice) | | **Maps** | [FromMap](#frommap) [ToMap](#tomap) [ToMapKV](#tomapkv) | | **Ordering** | [After](#after) [Before](#before) [Reverse](#reverse) [Shuffle](#shuffle) [Sort](#sort) | -| **Querying** | [All](#all) [Any](#any) [At](#at) [Contains](#contains) [FindWhere](#findwhere) [First](#first) [FirstWhere](#firstwhere) [IndexWhere](#indexwhere) [IsEmpty](#isempty) [Last](#last) [LastWhere](#lastwhere) [None](#none) | +| **Querying** | [All](#all) [Any](#any) [At](#at) [Contains](#contains) [First](#first) [FirstWhere](#firstwhere) [IndexWhere](#indexwhere) [IsEmpty](#isempty) [Last](#last) [LastWhere](#lastwhere) [None](#none) | | **Serialization** | [ToJSON](#tojson) [ToPrettyJSON](#toprettyjson) | | **Set Operations** | [Difference](#difference) [Intersect](#intersect) [SymmetricDifference](#symmetricdifference) [Union](#union) [Unique](#unique) [UniqueBy](#uniqueby) [UniqueComparable](#uniquecomparable) | -| **Slicing** | [Chunk](#chunk) [Filter](#filter) [Partition](#partition) [Pop](#pop) [PopN](#popn) [Skip](#skip) [SkipLast](#skiplast) [Take](#take) [TakeLast](#takelast) [TakeUntil](#takeuntil) [TakeUntilFn](#takeuntilfn) [Where](#where) [Window](#window) | -| **Transformation** | [Append](#append) [Concat](#concat) [Each](#each) [Map](#map) [MapTo](#mapto) [Merge](#merge) [Multiply](#multiply) [Pipe](#pipe) [Pluck](#pluck) [Prepend](#prepend) [Push](#push) [Tap](#tap) [Times](#times) [Transform](#transform) [Zip](#zip) [ZipWith](#zipwith) | +| **Slicing** | [Chunk](#chunk) [Filter](#filter) [Partition](#partition) [Pop](#pop) [PopN](#popn) [Skip](#skip) [SkipLast](#skiplast) [Take](#take) [TakeLast](#takelast) [TakeUntil](#takeuntil) [TakeUntilFn](#takeuntilfn) [Window](#window) | +| **Transformation** | [Append](#append) [Concat](#concat) [Each](#each) [Map](#map) [MapTo](#mapto) [Merge](#merge) [Multiply](#multiply) [Pipe](#pipe) [Prepend](#prepend) [Tap](#tap) [Times](#times) [Transform](#transform) [Zip](#zip) [ZipWith](#zipwith) | ## Access ### Items · readonly · fluent -Items returns the underlying slice of items. +Items returns the backing slice of items. _Example: integers_ @@ -331,6 +331,21 @@ collection.Dump(out) // ] ``` +### ItemsCopy · readonly · fluent + +ItemsCopy returns a copy of the collection's items. + +```go +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 @@ -845,9 +860,43 @@ collection.Dump(total3) ## Construction -### Clone · allocates · fluent +### Attach · immutable · fluent + +Attach wraps a slice without copying. + +```go +items := []int{1, 2, 3} +c := collection.Attach(items) + +items[0] = 9 +collection.Dump(c.Items()) +// #[]int [ +// 0 => 9 #int +// 1 => 2 #int +// 2 => 3 #int +// ] +``` + +### AttachNumeric · immutable · fluent + +AttachNumeric wraps a slice of numeric types without copying. + +```go +items := []int{1, 2, 3} +c := collection.AttachNumeric(items) + +items[0] = 9 +collection.Dump(c.Items()) +// #[]int [ +// 0 => 9 #int +// 1 => 2 #int +// 2 => 3 #int +// ] +``` + +### Clone · immutable · fluent -Clone returns a shallow copy of the collection. +Clone returns a copy of the collection. _Example: basic cloning_ @@ -855,7 +904,7 @@ _Example: basic cloning_ c := collection.New([]int{1, 2, 3}) clone := c.Clone() -clone.Push(4) +clone.Append(4) collection.Dump(c.Items()) // #[]int [ @@ -916,7 +965,6 @@ New creates a new Collection from the provided slice. ### NewNumeric · immutable · fluent NewNumeric wraps a slice of numeric types in a NumericCollection. -A shallow copy is made so that further operations don't mutate the original slice. ## Debugging @@ -1435,28 +1483,27 @@ collection.Dump(users.Items()) // ] ``` -### Shuffle · mutable · fluent +### Shuffle · immutable · fluent -Shuffle randomly shuffles the items in the collection in place -and returns the same collection for chaining. +Shuffle returns a shuffled copy of the collection. _Example: integers_ ```go c := collection.New([]int{1, 2, 3, 4, 5}) -c.Shuffle() -collection.Dump(c.Items()) +out1 := c.Shuffle() +collection.Dump(out1.Items()) ``` _Example: strings – chaining_ ```go -out := collection.New([]string{"a", "b", "c"}). +out2 := collection.New([]string{"a", "b", "c"}). Shuffle(). Append("d"). Items() -collection.Dump(out) +collection.Dump(out2) ``` _Example: structs_ @@ -1473,8 +1520,8 @@ users := collection.New([]User{ {ID: 4}, }) -users.Shuffle() -collection.Dump(users.Items()) +out3 := users.Shuffle() +collection.Dump(out3.Items()) ``` ### Sort · mutable · fluent @@ -1640,16 +1687,14 @@ collection.Dump(u, ok3) ### Contains · readonly · fluent -Contains returns true if any item satisfies the predicate. +Contains returns true if the collection contains the given value. _Example: integers_ ```go c := collection.New([]int{1, 2, 3, 4, 5}) -hasEven := c.Contains(func(v int) bool { - return v%2 == 0 -}) -collection.Dump(hasEven) +hasTwo := collection.Contains(c, 2) +collection.Dump(hasTwo) // true #bool ``` @@ -1657,101 +1702,11 @@ _Example: strings_ ```go c2 := collection.New([]string{"apple", "banana", "cherry"}) -hasBanana := c2.Contains(func(v string) bool { - return v == "banana" -}) +hasBanana := collection.Contains(c2, "banana") collection.Dump(hasBanana) // true #bool ``` -_Example: structs_ - -```go -type User struct { - ID int - Name string -} - -users := collection.New([]User{ - {ID: 1, Name: "Alice"}, - {ID: 2, Name: "Bob"}, - {ID: 3, Name: "Carol"}, -}) - -hasBob := users.Contains(func(u User) bool { - return u.Name == "Bob" -}) -collection.Dump(hasBob) -// true #bool -``` - -### FindWhere · readonly · fluent - -FindWhere returns the first item in the collection for which the provided -predicate function returns true. This is an alias for FirstWhere(fn) and -exists for ergonomic parity with functional languages (JavaScript, Rust, -C#, Python) where developers expect a “find” helper. - -_Example: integers_ - -```go -nums := collection.New([]int{1, 2, 3, 4, 5}) - -v1, ok1 := nums.FindWhere(func(n int) bool { - return n == 3 -}) -collection.Dump(v1, ok1) -// 3 #int -// true #bool -``` - -_Example: no match_ - -```go -v2, ok2 := nums.FindWhere(func(n int) bool { - return n > 10 -}) -collection.Dump(v2, ok2) -// 0 #int -// false #bool -``` - -_Example: structs_ - -```go -type User struct { - ID int - Name string -} - -users := collection.New([]User{ - {ID: 1, Name: "Alice"}, - {ID: 2, Name: "Bob"}, - {ID: 3, Name: "Charlie"}, -}) - -u, ok3 := users.FindWhere(func(u User) bool { - return u.ID == 2 -}) -collection.Dump(u, ok3) -// #collection.User { -// +ID => 2 #int -// +Name => "Bob" #string -// } -// true #bool -``` - -_Example: integers - empty collection_ - -```go -empty := collection.New([]int{}) - -v4, ok4 := empty.FindWhere(func(n int) bool { return n == 1 }) -collection.Dump(v4, ok4) -// 0 #int -// false #bool -``` - ### First · readonly · fluent First returns the first element in the collection. @@ -2790,18 +2745,18 @@ collection.Dump(active.Items(), inactive.Items()) // ] ``` -### Pop · mutable · fluent +### Pop · mutable -Pop returns the last item and a new collection with that item removed. -The original collection remains unchanged. +Pop removes and returns the last item in the collection. _Example: integers_ ```go c := collection.New([]int{1, 2, 3}) -item, rest := c.Pop() -collection.Dump(item, rest.Items()) +item, ok := c.Pop() +collection.Dump(item, ok, c.Items()) // 3 #int +// true #bool // #[]int [ // 0 => 1 #int // 1 => 2 #int @@ -2812,9 +2767,10 @@ _Example: strings_ ```go c2 := collection.New([]string{"a", "b", "c"}) -item2, rest2 := c2.Pop() -collection.Dump(item2, rest2.Items()) +item2, ok2 := c2.Pop() +collection.Dump(item2, ok2, c2.Items()) // "c" #string +// true #bool // #[]string [ // 0 => "a" #string // 1 => "b" #string @@ -2834,12 +2790,13 @@ users := collection.New([]User{ {ID: 2, Name: "Bob"}, }) -item3, rest3 := users.Pop() -collection.Dump(item3, rest3.Items()) +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 @@ -2852,27 +2809,27 @@ _Example: empty collection_ ```go empty := collection.New([]int{}) -item4, rest4 := empty.Pop() -collection.Dump(item4, rest4.Items()) +item4, ok4 := empty.Pop() +collection.Dump(item4, ok4, empty.Items()) // 0 #int +// false #bool // #[]int [ // ] ``` -### PopN · mutable · fluent +### PopN · mutable -PopN removes and returns the last n items as a new collection, -and returns a second collection containing the remaining items. +PopN removes and returns the last n items in original order. _Example: integers – pop 2_ ```go c := collection.New([]int{1, 2, 3, 4}) -popped, rest := c.PopN(2) -collection.Dump(popped.Items(), rest.Items()) +popped := c.PopN(2) +collection.Dump(popped, c.Items()) // #[]int [ -// 0 => 4 #int -// 1 => 3 #int +// 0 => 3 #int +// 1 => 4 #int // ] // #[]int [ // 0 => 1 #int @@ -2884,8 +2841,8 @@ _Example: strings – pop 1_ ```go c2 := collection.New([]string{"a", "b", "c"}) -popped2, rest2 := c2.PopN(1) -collection.Dump(popped2.Items(), rest2.Items()) +popped2 := c2.PopN(1) +collection.Dump(popped2, c2.Items()) // #[]string [ // 0 => "c" #string // ] @@ -2909,17 +2866,17 @@ users := collection.New([]User{ {ID: 3, Name: "Carol"}, }) -popped3, rest3 := users.PopN(2) -collection.Dump(popped3.Items(), rest3.Items()) +popped3 := users.PopN(2) +collection.Dump(popped3, users.Items()) // #[]main.User [ // 0 => #main.User { -// +ID => 3 #int -// +Name => "Carol" #string -// } -// 1 => #main.User { // +ID => 2 #int // +Name => "Bob" #string // } +// 1 => #main.User { +// +ID => 3 #int +// +Name => "Carol" #string +// } // ] // #[]main.User [ // 0 => #main.User { @@ -2929,14 +2886,13 @@ collection.Dump(popped3.Items(), rest3.Items()) // ] ``` -_Example: integers - n <= 0 → returns empty popped + original collection_ +_Example: integers - n <= 0 → returns nil, no change_ ```go c3 := collection.New([]int{1, 2, 3}) -popped4, rest4 := c3.PopN(0) -collection.Dump(popped4.Items(), rest4.Items()) -// #[]int [ -// ] +popped4 := c3.PopN(0) +collection.Dump(popped4, c3.Items()) +// // #[]int [ // 0 => 1 #int // 1 => 2 #int @@ -2948,11 +2904,11 @@ _Example: strings - n exceeds length → all items popped, rest empty_ ```go c4 := collection.New([]string{"x", "y"}) -popped5, rest5 := c4.PopN(10) -collection.Dump(popped5.Items(), rest5.Items()) +popped5 := c4.PopN(10) +collection.Dump(popped5, c4.Items()) // #[]string [ -// 0 => "y" #string -// 1 => "x" #string +// 0 => "x" #string +// 1 => "y" #string // ] // #[]string [ // ] @@ -3281,57 +3237,6 @@ collection.Dump(out3.Items()) // ] ``` -### Where · mutable · fluent - -Where keeps only the elements for which fn returns true. -This is an alias for Filter(fn) for SQL-style ergonomics. -This method mutates the collection in place and returns the same instance. - -_Example: integers_ - -```go -nums := collection.New([]int{1, 2, 3, 4}) -nums.Where(func(v int) bool { - return v%2 == 0 -}) -collection.Dump(nums.Items()) -// #[]int [ -// 0 => 2 #int -// 1 => 4 #int -// ] -``` - -_Example: structs_ - -```go -type User struct { - ID int - Name string -} - -users := collection.New([]User{ - {ID: 1, Name: "Alice"}, - {ID: 2, Name: "Bob"}, - {ID: 3, Name: "Carol"}, -}) - -users.Where(func(u User) bool { - return u.ID >= 2 -}) - -collection.Dump(users.Items()) -// #[]main.User [ -// 0 => #main.User { -// +ID => 2 #int -// +Name => "Bob" #string -// } -// 1 => #main.User { -// +ID => 3 #int -// +Name => "Carol" #string -// } -// ] -``` - ### Window · allocates · fluent Window returns overlapping (or stepped) windows of the collection. @@ -3508,7 +3413,7 @@ collection.Dump(concatenated) // ] ``` -### Each · immutable · fluent +### Each · readonly · fluent Each runs fn for every item in the collection and returns the same collection, so it can be used in chains for side effects (logging, debugging, etc.). @@ -3703,9 +3608,9 @@ collection.Dump(names.Items()) // ] ``` -### Merge · mutable · fluent +### Merge · immutable · fluent -Merge merges the given data into the current collection. +Merge merges the given data into a new collection. _Example: integers - merging slices_ @@ -3779,7 +3684,7 @@ collection.Dump(merged3.Items()) // ] ``` -### Multiply · mutable · fluent +### Multiply · immutable · fluent Multiply creates `n` copies of all items in the collection and returns a new collection. @@ -3858,7 +3763,7 @@ _Example: integers – computing a sum_ ```go c := collection.New([]int{1, 2, 3}) -sum := c.Pipe(func(col *collection.Collection[int]) any { +sum := collection.Pipe(c, func(col *collection.Collection[int]) int { total := 0 for _, v := range col.Items() { total += v @@ -3873,7 +3778,7 @@ _Example: strings – joining values_ ```go c2 := collection.New([]string{"a", "b", "c"}) -joined := c2.Pipe(func(col *collection.Collection[string]) any { +joined := collection.Pipe(c2, func(col *collection.Collection[string]) string { out := "" for _, v := range col.Items() { out += v @@ -3897,7 +3802,7 @@ users := collection.New([]User{ {ID: 2, Name: "Bob"}, }) -names := users.Pipe(func(col *collection.Collection[User]) any { +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) @@ -3912,81 +3817,16 @@ collection.Dump(names) // ] ``` -### Pluck · immutable · fluent - -Pluck is an alias for MapTo with a more semantic name when projecting fields. -It extracts a single field or computed value from every element and returns a -new typed collection. - -_Example: integers - extract parity label_ - -```go -nums := collection.New([]int{1, 2, 3, 4}) -parity := collection.Pluck(nums, func(n int) string { - if n%2 == 0 { - return "even" - } - return "odd" -}) -collection.Dump(parity.Items()) -// #[]string [ -// 0 => "odd" #string -// 1 => "even" #string -// 2 => "odd" #string -// 3 => "even" #string -// ] -``` - -_Example: strings - length of each value_ - -```go -words := collection.New([]string{"go", "forj", "rocks"}) -lengths := collection.Pluck(words, func(s string) int { - return len(s) -}) -collection.Dump(lengths.Items()) -// #[]int [ -// 0 => 2 #int -// 1 => 4 #int -// 2 => 5 #int -// ] -``` - -_Example: structs - pluck a field_ - -```go -type User struct { - ID int - Name string -} - -users := collection.New([]User{ - {ID: 1, Name: "Alice"}, - {ID: 2, Name: "Bob"}, -}) - -names := collection.Pluck(users, func(u User) string { - return u.Name -}) - -collection.Dump(names.Items()) -// #[]string [ -// 0 => "Alice" #string -// 1 => "Bob" #string -// ] -``` - ### Prepend · mutable · fluent -Prepend returns a new collection with the given values added -to the *beginning* of the collection. +Prepend adds the given values to the beginning of the collection. _Example: integers_ ```go c := collection.New([]int{3, 4}) -newC := c.Prepend(1, 2) -collection.Dump(newC.Items()) +c.Prepend(1, 2) +collection.Dump(c.Items()) // #[]int [ // 0 => 1 #int // 1 => 2 #int @@ -3999,8 +3839,8 @@ _Example: strings_ ```go letters := collection.New([]string{"c", "d"}) -out := letters.Prepend("a", "b") -collection.Dump(out.Items()) +letters.Prepend("a", "b") +collection.Dump(letters.Items()) // #[]string [ // 0 => "a" #string // 1 => "b" #string @@ -4021,8 +3861,8 @@ users := collection.New([]User{ {ID: 2, Name: "Bob"}, }) -out2 := users.Prepend(User{ID: 1, Name: "Alice"}) -collection.Dump(out2.Items()) +users.Prepend(User{ID: 1, Name: "Alice"}) +collection.Dump(users.Items()) // #[]main.User [ // 0 => #main.User { // +ID => 1 #int @@ -4039,74 +3879,26 @@ _Example: integers - Prepending into an empty collection_ ```go empty := collection.New([]int{}) -out3 := empty.Prepend(9, 8) -collection.Dump(out3.Items()) +empty.Prepend(9, 8) +collection.Dump(empty.Items()) // #[]int [ // 0 => 9 #int // 1 => 8 #int // ] ``` -_Example: integers - Prepending no values → returns a copy of original_ +_Example: integers - Prepending no values → no change_ ```go c2 := collection.New([]int{1, 2}) -out4 := c2.Prepend() -collection.Dump(out4.Items()) +c2.Prepend() +collection.Dump(c2.Items()) // #[]int [ // 0 => 1 #int // 1 => 2 #int // ] ``` -### Push · immutable · fluent - -Push returns a new collection with the given values appended. - -```go -nums := collection.New([]int{1, 2}).Push(3, 4) -nums.Dump() -// #[]int [ -// 0 => 1 #int -// 1 => 2 #int -// 2 => 3 #int -// 3 => 4 #int -// ] - -// Complex type (structs) -type User struct { - Name string - Age int -} - -users := collection.New([]User{ - {Name: "Alice", Age: 30}, - {Name: "Bob", Age: 25}, -}).Push( - User{Name: "Carol", Age: 40}, - User{Name: "Dave", Age: 20}, -) -users.Dump() -// #[]main.User [ -// 0 => #main.User { -// +Name => "Alice" #string -// +Age => 30 #int -// } -// 1 => #main.User { -// +Name => "Bob" #string -// +Age => 25 #int -// } -// 2 => #main.User { -// +Name => "Carol" #string -// +Age => 40 #int -// } -// 3 => #main.User { -// +Name => "Dave" #string -// +Age => 20 #int -// } -// ] -``` - ### Tap · immutable · fluent Tap invokes fn with the collection pointer for side effects (logging, debugging, diff --git a/after.go b/after.go index c2c0bfb..7e64a90 100644 --- a/after.go +++ b/after.go @@ -6,6 +6,8 @@ package collection // @behavior immutable // @fluent true // +// NOTE: returns a view (shares backing array). Use Clone() to detach. +// // Example: integers // // c := collection.New([]int{1, 2, 3, 4, 5}) @@ -25,10 +27,8 @@ func (c *Collection[T]) After(pred func(T) bool) *Collection[T] { // If no match found → empty collection if idx == -1 || idx+1 >= len(c.items) { - return &Collection[T]{items: []T{}} + return Attach(c.items[:0]) } - out := make([]T, len(c.items)-(idx+1)) - copy(out, c.items[idx+1:]) - return &Collection[T]{items: out} + return Attach(c.items[idx+1:]) } diff --git a/append.go b/append.go index 18dd7d2..6efbf43 100644 --- a/append.go +++ b/append.go @@ -54,57 +54,5 @@ func (c *Collection[T]) Append(values ...T) *Collection[T] { out := make([]T, 0, len(c.items)+len(values)) out = append(out, c.items...) out = append(out, values...) - return &Collection[T]{items: out} -} - -// Push returns a new collection with the given values appended. -// @group Transformation -// @behavior immutable -// @fluent true -// -// Example: integers -// -// nums := collection.New([]int{1, 2}).Push(3, 4) -// nums.Dump() -// // #[]int [ -// // 0 => 1 #int -// // 1 => 2 #int -// // 2 => 3 #int -// // 3 => 4 #int -// // ] -// -// // Complex type (structs) -// type User struct { -// Name string -// Age int -// } -// -// users := collection.New([]User{ -// {Name: "Alice", Age: 30}, -// {Name: "Bob", Age: 25}, -// }).Push( -// User{Name: "Carol", Age: 40}, -// User{Name: "Dave", Age: 20}, -// ) -// users.Dump() -// // #[]main.User [ -// // 0 => #main.User { -// // +Name => "Alice" #string -// // +Age => 30 #int -// // } -// // 1 => #main.User { -// // +Name => "Bob" #string -// // +Age => 25 #int -// // } -// // 2 => #main.User { -// // +Name => "Carol" #string -// // +Age => 40 #int -// // } -// // 3 => #main.User { -// // +Name => "Dave" #string -// // +Age => 20 #int -// // } -// // ] -func (c *Collection[T]) Push(values ...T) *Collection[T] { - return c.Append(values...) + return Attach(out) } diff --git a/append_test.go b/append_test.go index 935832e..afbc9f2 100644 --- a/append_test.go +++ b/append_test.go @@ -5,98 +5,76 @@ import ( "testing" ) -func TestAppendAndPush(t *testing.T) { - ops := []struct { - name string - fn func(*Collection[int], ...int) *Collection[int] - }{ - {"Append", (*Collection[int]).Append}, - {"Push", (*Collection[int]).Push}, // alias - } - - for _, op := range ops { - t.Run(op.name+"_Basic", func(t *testing.T) { - c := New([]int{1, 2}) +func TestAppend(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + c := New([]int{1, 2}) - out := op.fn(c, 3, 4) - expected := []int{1, 2, 3, 4} + out := c.Append(3, 4) + expected := []int{1, 2, 3, 4} - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("%s basic expected %v, got %v", op.name, expected, out.items) - } - }) + if !reflect.DeepEqual(out.items, expected) { + t.Fatalf("Append basic expected %v, got %v", expected, out.items) + } + }) - t.Run(op.name+"_EmptyCollection", func(t *testing.T) { - c := New([]int{}) + t.Run("EmptyCollection", func(t *testing.T) { + c := New([]int{}) - out := op.fn(c, 5, 6) - expected := []int{5, 6} + out := c.Append(5, 6) + expected := []int{5, 6} - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("%s empty expected %v, got %v", op.name, expected, out.items) - } - }) + if !reflect.DeepEqual(out.items, expected) { + t.Fatalf("Append empty expected %v, got %v", expected, out.items) + } + }) - t.Run(op.name+"_NoValues", func(t *testing.T) { - c := New([]int{10, 20, 30}) + t.Run("NoValues", func(t *testing.T) { + c := New([]int{10, 20, 30}) - out := op.fn(c) // no-op - expected := []int{10, 20, 30} + out := c.Append() // no-op + expected := []int{10, 20, 30} - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("%s no-values expected %v, got %v", op.name, expected, out.items) - } - }) + if !reflect.DeepEqual(out.items, expected) { + t.Fatalf("Append no-values expected %v, got %v", expected, out.items) + } + }) - t.Run(op.name+"_NoMutation", func(t *testing.T) { - orig := []int{1, 2, 3} - c := New(orig) + t.Run("NoMutation", func(t *testing.T) { + orig := []int{1, 2, 3} + c := New(orig) - _ = op.fn(c, 4, 5) + _ = c.Append(4, 5) - if !reflect.DeepEqual(c.items, orig) { - t.Fatalf("%s mutated original %v", op.name, c.items) - } - }) - } + if !reflect.DeepEqual(c.items, orig) { + t.Fatalf("Append mutated original %v", c.items) + } + }) } -func TestAppendAndPush_Structs(t *testing.T) { +func TestAppend_Structs(t *testing.T) { type User struct { ID int Name string } - ops := []struct { - name string - fn func(*Collection[User], ...User) *Collection[User] - }{ - {"Append", (*Collection[User]).Append}, - {"Push", (*Collection[User]).Push}, // alias + c := New([]User{ + {1, "Chris"}, + {2, "Van"}, + }) + + out := c.Append( + User{3, "Shawn"}, + User{4, "Matt"}, + ) + + expected := []User{ + {1, "Chris"}, + {2, "Van"}, + {3, "Shawn"}, + {4, "Matt"}, } - for _, op := range ops { - t.Run(op.name+"_Structs", func(t *testing.T) { - c := New([]User{ - {1, "Chris"}, - {2, "Van"}, - }) - - out := op.fn(c, - User{3, "Shawn"}, - User{4, "Matt"}, - ) - - expected := []User{ - {1, "Chris"}, - {2, "Van"}, - {3, "Shawn"}, - {4, "Matt"}, - } - - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("%s structs expected %v, got %v", op.name, expected, out.items) - } - }) + if !reflect.DeepEqual(out.items, expected) { + t.Fatalf("Append structs expected %v, got %v", expected, out.items) } } diff --git a/before.go b/before.go index 8cb0ac0..aa691cc 100644 --- a/before.go +++ b/before.go @@ -6,6 +6,8 @@ package collection // @behavior immutable // @fluent true // +// NOTE: returns a view (shares backing array). Use Clone() to detach. +// // If no element matches the predicate, the entire collection is returned. // // Example: integers @@ -58,7 +60,5 @@ func (c *Collection[T]) Before(pred func(T) bool) *Collection[T] { } } - out := make([]T, idx) - copy(out, c.items[:idx]) - return &Collection[T]{items: out} + return Attach(c.items[:idx]) } diff --git a/chunk.go b/chunk.go index 7a9f352..817aeb0 100644 --- a/chunk.go +++ b/chunk.go @@ -7,6 +7,8 @@ package collection // @fluent true // // If size <= 0, nil is returned. +// +// NOTE: chunks share the backing array with the source collection. // Example: integers // // c := collection.New([]int{1, 2, 3, 4, 5}).Chunk(2) diff --git a/clone.go b/clone.go index 91b547b..06f938e 100644 --- a/clone.go +++ b/clone.go @@ -1,6 +1,6 @@ package collection -// Clone returns a shallow copy of the collection. +// Clone returns a copy of the collection. // @fluent true // // The returned collection has its own backing slice, so subsequent mutations @@ -10,14 +10,14 @@ package collection // the original collection. // // @group Construction -// @behavior allocates +// @behavior immutable // // Example: basic cloning // // c := collection.New([]int{1, 2, 3}) // clone := c.Clone() // -// clone.Push(4) +// clone.Append(4) // // collection.Dump(c.Items()) // // #[]int [ @@ -70,5 +70,5 @@ package collection func (c *Collection[T]) Clone() *Collection[T] { out := make([]T, len(c.items)) copy(out, c.items) - return &Collection[T]{items: out} + return Attach(out) } diff --git a/collection.go b/collection.go index 6dc3ef8..fa3a5a2 100644 --- a/collection.go +++ b/collection.go @@ -30,11 +30,16 @@ type Pair[K comparable, V any] struct { // around the slice, enabling fluent, chainable operations such as // filtering, mapping, reducing, sorting, and more. // -// The underlying slice is stored as-is (no copy is made), allowing -// New to be both fast and allocation-friendly. Callers should clone -// the input beforehand if they need to prevent shared mutation. +// New copies the input slice to avoid shared backing. func New[T any](items []T) *Collection[T] { - return &Collection[T]{items: items} + var out []T + if items == nil { + out = nil + } else { + out = make([]T, len(items)) + copy(out, items) + } + return &Collection[T]{items: out} } // NumericCollection is a Collection specialized for numeric types. @@ -43,21 +48,80 @@ type NumericCollection[T Number] struct { } // NewNumeric wraps a slice of numeric types in a NumericCollection. -// A shallow copy is made so that further operations don't mutate the original slice. // @group Construction // @behavior immutable // @fluent true +// +// NewNumeric copies the input slice to avoid shared backing. func NewNumeric[T Number](items []T) *NumericCollection[T] { + var out []T + if items == nil { + out = nil + } else { + out = make([]T, len(items)) + copy(out, items) + } return &NumericCollection[T]{ - Collection: &Collection[T]{items: items}, + Collection: &Collection[T]{items: out}, } } -// Items returns the underlying slice of items. +// Attach wraps a slice without copying. +// @group Construction +// @behavior immutable +// @fluent true +// +// Attach shares the backing array with the caller. Mutating either side +// will affect the other. Use New to copy the input. +// +// Example: sharing backing slice +// +// items := []int{1, 2, 3} +// c := collection.Attach(items) +// +// items[0] = 9 +// collection.Dump(c.Items()) +// // #[]int [ +// // 0 => 9 #int +// // 1 => 2 #int +// // 2 => 3 #int +// // ] +func Attach[T any](items []T) *Collection[T] { + return &Collection[T]{items: items} +} + +// AttachNumeric wraps a slice of numeric types without copying. +// @group Construction +// @behavior immutable +// @fluent true +// +// AttachNumeric shares the backing array with the caller. Mutating either side +// will affect the other. Use NewNumeric to copy the input. +// +// Example: sharing backing slice +// +// items := []int{1, 2, 3} +// c := collection.AttachNumeric(items) +// +// items[0] = 9 +// collection.Dump(c.Items()) +// // #[]int [ +// // 0 => 9 #int +// // 1 => 2 #int +// // 2 => 3 #int +// // ] +func AttachNumeric[T Number](items []T) *NumericCollection[T] { + return &NumericCollection[T]{Collection: &Collection[T]{items: items}} +} + +// Items returns the backing slice of items. // @group Access // @behavior readonly // @fluent true // +// Items shares the backing array with the collection. Mutating the returned +// slice will mutate the collection. +// // Example: integers // // c := collection.New([]int{1, 2, 3}) @@ -106,3 +170,26 @@ func NewNumeric[T Number](items []T) *NumericCollection[T] { func (c *Collection[T]) Items() []T { return c.items } + +// ItemsCopy returns a copy of the collection's items. +// @group Access +// @behavior readonly +// @fluent true +// +// ItemsCopy allocates a new slice. +// +// Example: integers +// +// c := collection.New([]int{1, 2, 3}) +// items := c.ItemsCopy() +// collection.Dump(items) +// // #[]int [ +// // 0 => 1 #int +// // 1 => 2 #int +// // 2 => 3 #int +// // ] +func (c *Collection[T]) ItemsCopy() []T { + out := make([]T, len(c.items)) + copy(out, c.items) + return out +} diff --git a/collection_test.go b/collection_test.go index 232d46b..ffeef5a 100644 --- a/collection_test.go +++ b/collection_test.go @@ -22,8 +22,8 @@ func TestFluentChainWithStructs(t *testing.T) { Filter(func(u User) bool { return u.Age >= 35 }). Sort(func(a, b User) bool { return a.Age < b.Age }) - // Type change happens at the edge using MapTo/Pluck. - names := Pluck(filteredAndSorted, func(u User) string { + // Type change happens at the edge using MapTo. + names := MapTo(filteredAndSorted, func(u User) string { return u.Name }).Items() diff --git a/constructor_test.go b/constructor_test.go new file mode 100644 index 0000000..076a1f6 --- /dev/null +++ b/constructor_test.go @@ -0,0 +1,89 @@ +package collection + +import "testing" + +func TestNew_CopiesInputSlice(t *testing.T) { + items := []int{1, 2, 3} + c := New(items) + + items[0] = 9 + + if c.Items()[0] == 9 { + t.Fatalf("New should copy input slice") + } +} + +func TestNew_PreservesNilSlice(t *testing.T) { + var items []int + c := New(items) + + if c.Items() != nil { + t.Fatalf("New should preserve nil slice") + } +} + +func TestAttach_SharesBackingSlice(t *testing.T) { + items := []int{1, 2, 3} + c := Attach(items) + + items[0] = 9 + + if c.Items()[0] != 9 { + t.Fatalf("Attach should share backing slice") + } +} + +func TestNewNumeric_PreservesNilSlice(t *testing.T) { + var items []int + c := NewNumeric(items) + + if c.Items() != nil { + t.Fatalf("NewNumeric should preserve nil slice") + } +} + +func TestNewNumeric_CopiesInputSlice(t *testing.T) { + items := []int{1, 2, 3} + c := NewNumeric(items) + + items[0] = 9 + + if c.Items()[0] == 9 { + t.Fatalf("NewNumeric should copy input slice") + } +} + +func TestAttachNumeric_SharesBackingSlice(t *testing.T) { + items := []int{1, 2, 3} + c := AttachNumeric(items) + + items[0] = 9 + + if c.Items()[0] != 9 { + t.Fatalf("AttachNumeric should share backing slice") + } +} + +func TestSelectionOps_ShareBackingSlice(t *testing.T) { + items := []int{1, 2, 3, 4} + c := Attach(items) + + view := c.Take(2) + items[0] = 9 + + if view.Items()[0] != 9 { + t.Fatalf("selection ops should return views") + } +} + +func TestItemsCopy_ReturnsCopy(t *testing.T) { + items := []int{1, 2, 3} + c := Attach(items) + + copyItems := c.ItemsCopy() + copyItems[0] = 9 + + if c.Items()[0] == 9 { + t.Fatalf("ItemsCopy should return a copy") + } +} diff --git a/contains.go b/contains.go index 40354ff..79ce741 100644 --- a/contains.go +++ b/contains.go @@ -1,48 +1,28 @@ package collection -// Contains returns true if any item satisfies the predicate. +// Contains returns true if the collection contains the given value. // @group Querying // @behavior readonly // @fluent true +// +// Similar to: Any (differs by using equality on a value). +// // Example: integers // // c := collection.New([]int{1, 2, 3, 4, 5}) -// hasEven := c.Contains(func(v int) bool { -// return v%2 == 0 -// }) -// collection.Dump(hasEven) +// hasTwo := collection.Contains(c, 2) +// collection.Dump(hasTwo) // // true #bool // // Example: strings // // c2 := collection.New([]string{"apple", "banana", "cherry"}) -// hasBanana := c2.Contains(func(v string) bool { -// return v == "banana" -// }) +// hasBanana := collection.Contains(c2, "banana") // collection.Dump(hasBanana) // // true #bool -// -// Example: structs -// -// type User struct { -// ID int -// Name string -// } -// -// users := collection.New([]User{ -// {ID: 1, Name: "Alice"}, -// {ID: 2, Name: "Bob"}, -// {ID: 3, Name: "Carol"}, -// }) -// -// hasBob := users.Contains(func(u User) bool { -// return u.Name == "Bob" -// }) -// collection.Dump(hasBob) -// // true #bool -func (c *Collection[T]) Contains(pred func(T) bool) bool { +func Contains[T comparable](c *Collection[T], value T) bool { for _, v := range c.items { - if pred(v) { + if v == value { return true } } diff --git a/contains_test.go b/contains_test.go index f2f51db..9fcfdff 100644 --- a/contains_test.go +++ b/contains_test.go @@ -5,7 +5,7 @@ import "testing" func TestContains_ValueMatch(t *testing.T) { nums := New([]int{1, 2, 3}) - if !nums.Contains(func(v int) bool { return v == 2 }) { + if !Contains(nums, 2) { t.Fatalf("expected true, got false") } } @@ -13,7 +13,7 @@ func TestContains_ValueMatch(t *testing.T) { func TestContains_NoMatch(t *testing.T) { nums := New([]int{1, 2, 3}) - if nums.Contains(func(v int) bool { return v == 99 }) { + if Contains(nums, 99) { t.Fatalf("expected false, got true") } } @@ -21,7 +21,7 @@ func TestContains_NoMatch(t *testing.T) { func TestContains_EmptyCollection(t *testing.T) { nums := New([]int{}) - if nums.Contains(func(v int) bool { return true }) { + if Contains(nums, 1) { t.Fatalf("expected false for empty collection") } } @@ -38,11 +38,11 @@ func TestContains_Structs(t *testing.T) { {3, "Shawn"}, }) - if !users.Contains(func(u User) bool { return u.Name == "Van" }) { + if !Contains(users, User{ID: 2, Name: "Van"}) { t.Fatalf("expected true, got false") } - if users.Contains(func(u User) bool { return u.Name == "Zach" }) { + if Contains(users, User{ID: 99, Name: "Zach"}) { t.Fatalf("expected false, got true") } } diff --git a/difference.go b/difference.go index 7781c15..ab45c1a 100644 --- a/difference.go +++ b/difference.go @@ -62,7 +62,7 @@ package collection // // ] func Difference[T comparable](a, b *Collection[T]) *Collection[T] { if len(a.items) == 0 { - return New([]T{}) + return Attach([]T{}) } lookup := make(map[T]struct{}, len(b.items)) @@ -84,5 +84,5 @@ func Difference[T comparable](a, b *Collection[T]) *Collection[T] { out = append(out, v) } - return &Collection[T]{items: out} + return Attach(out) } diff --git a/docs/bench/main.go b/docs/bench/main.go index 3341ac2..2bfffeb 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -393,7 +393,7 @@ func benchTakeLo(b *testing.B) { func benchContainsCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.New(benchInts).Contains(func(v int) bool { return v == benchSize-1 }) + _ = collection.Contains(collection.New(benchInts), benchSize-1) } } diff --git a/each.go b/each.go index 3197480..df439d3 100644 --- a/each.go +++ b/each.go @@ -3,7 +3,7 @@ package collection // 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.). // @group Transformation -// @behavior immutable +// @behavior readonly // @fluent true // // Example: integers diff --git a/each_test.go b/each_test.go index e621230..6fa249c 100644 --- a/each_test.go +++ b/each_test.go @@ -23,15 +23,8 @@ func TestEach_ReturnsSameCollection(t *testing.T) { out := c.Each(func(v int) {}) - // They should hold identical items - if !reflect.DeepEqual(out.items, c.items) { - t.Fatalf("Each should return the same items: %v vs %v", out.items, c.items) - } - - // But ensure it's the same collection struct (value semantics) - // Structs compare equal by fields, so this is fine. - if out.Count() != c.Count() { - t.Fatalf("collection count mismatch") + if out != c { + t.Fatalf("Each should return the same collection instance") } } diff --git a/examples/attach/main.go b/examples/attach/main.go new file mode 100644 index 0000000..abd5f6b --- /dev/null +++ b/examples/attach/main.go @@ -0,0 +1,22 @@ +//go:build ignore +// +build ignore + +package main + +import "github.com/goforj/collection" + +func main() { + // Attach wraps a slice without copying. + + // Example: sharing backing slice + items := []int{1, 2, 3} + c := collection.Attach(items) + + items[0] = 9 + collection.Dump(c.Items()) + // #[]int [ + // 0 => 9 #int + // 1 => 2 #int + // 2 => 3 #int + // ] +} diff --git a/examples/attachnumeric/main.go b/examples/attachnumeric/main.go new file mode 100644 index 0000000..5515675 --- /dev/null +++ b/examples/attachnumeric/main.go @@ -0,0 +1,22 @@ +//go:build ignore +// +build ignore + +package main + +import "github.com/goforj/collection" + +func main() { + // AttachNumeric wraps a slice of numeric types without copying. + + // Example: sharing backing slice + items := []int{1, 2, 3} + c := collection.AttachNumeric(items) + + items[0] = 9 + collection.Dump(c.Items()) + // #[]int [ + // 0 => 9 #int + // 1 => 2 #int + // 2 => 3 #int + // ] +} diff --git a/examples/clone/main.go b/examples/clone/main.go index d1e348b..e672100 100644 --- a/examples/clone/main.go +++ b/examples/clone/main.go @@ -6,7 +6,7 @@ package main import "github.com/goforj/collection" func main() { - // Clone returns a shallow copy of the collection. + // Clone returns a copy of the collection. // @fluent true // // The returned collection has its own backing slice, so subsequent mutations @@ -19,7 +19,7 @@ func main() { c := collection.New([]int{1, 2, 3}) clone := c.Clone() - clone.Push(4) + clone.Append(4) collection.Dump(c.Items()) // #[]int [ diff --git a/examples/contains/main.go b/examples/contains/main.go index 311b3cd..3c40a74 100644 --- a/examples/contains/main.go +++ b/examples/contains/main.go @@ -6,39 +6,17 @@ package main import "github.com/goforj/collection" func main() { - // Contains returns true if any item satisfies the predicate. + // Contains returns true if the collection contains the given value. // Example: integers c := collection.New([]int{1, 2, 3, 4, 5}) - hasEven := c.Contains(func(v int) bool { - return v%2 == 0 - }) - collection.Dump(hasEven) + hasTwo := collection.Contains(c, 2) + collection.Dump(hasTwo) // true #bool // Example: strings c2 := collection.New([]string{"apple", "banana", "cherry"}) - hasBanana := c2.Contains(func(v string) bool { - return v == "banana" - }) + hasBanana := collection.Contains(c2, "banana") collection.Dump(hasBanana) // true #bool - - // Example: structs - type User struct { - ID int - Name string - } - - users := collection.New([]User{ - {ID: 1, Name: "Alice"}, - {ID: 2, Name: "Bob"}, - {ID: 3, Name: "Carol"}, - }) - - hasBob := users.Contains(func(u User) bool { - return u.Name == "Bob" - }) - collection.Dump(hasBob) - // true #bool } diff --git a/examples/findwhere/main.go b/examples/findwhere/main.go deleted file mode 100644 index 907dc91..0000000 --- a/examples/findwhere/main.go +++ /dev/null @@ -1,61 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import "github.com/goforj/collection" - -func main() { - // FindWhere returns the first item in the collection for which the provided - // predicate function returns true. This is an alias for FirstWhere(fn) and - // exists for ergonomic parity with functional languages (JavaScript, Rust, - // C#, Python) where developers expect a “find” helper. - - // Example: integers - nums := collection.New([]int{1, 2, 3, 4, 5}) - - v1, ok1 := nums.FindWhere(func(n int) bool { - return n == 3 - }) - collection.Dump(v1, ok1) - // 3 #int - // true #bool - - // Example: no match - v2, ok2 := nums.FindWhere(func(n int) bool { - return n > 10 - }) - collection.Dump(v2, ok2) - // 0 #int - // false #bool - - // Example: structs - type User struct { - ID int - Name string - } - - users := collection.New([]User{ - {ID: 1, Name: "Alice"}, - {ID: 2, Name: "Bob"}, - {ID: 3, Name: "Charlie"}, - }) - - u, ok3 := users.FindWhere(func(u User) bool { - return u.ID == 2 - }) - collection.Dump(u, ok3) - // #collection.User { - // +ID => 2 #int - // +Name => "Bob" #string - // } - // true #bool - - // Example: integers - empty collection - empty := collection.New([]int{}) - - v4, ok4 := empty.FindWhere(func(n int) bool { return n == 1 }) - collection.Dump(v4, ok4) - // 0 #int - // false #bool -} diff --git a/examples/items/main.go b/examples/items/main.go index f86803a..7ef77b4 100644 --- a/examples/items/main.go +++ b/examples/items/main.go @@ -6,7 +6,7 @@ package main import "github.com/goforj/collection" func main() { - // Items returns the underlying slice of items. + // Items returns the backing slice of items. // Example: integers c := collection.New([]int{1, 2, 3}) diff --git a/examples/itemscopy/main.go b/examples/itemscopy/main.go new file mode 100644 index 0000000..69bb103 --- /dev/null +++ b/examples/itemscopy/main.go @@ -0,0 +1,20 @@ +//go:build ignore +// +build ignore + +package main + +import "github.com/goforj/collection" + +func main() { + // ItemsCopy returns a copy of the collection's items. + + // Example: integers + c := collection.New([]int{1, 2, 3}) + items := c.ItemsCopy() + collection.Dump(items) + // #[]int [ + // 0 => 1 #int + // 1 => 2 #int + // 2 => 3 #int + // ] +} diff --git a/examples/merge/main.go b/examples/merge/main.go index 9fdae34..2909dd5 100644 --- a/examples/merge/main.go +++ b/examples/merge/main.go @@ -6,7 +6,7 @@ package main import "github.com/goforj/collection" func main() { - // Merge merges the given data into the current collection. + // Merge merges the given data into a new collection. // Example: integers - merging slices ints := collection.New([]int{1, 2}) diff --git a/examples/pipe/main.go b/examples/pipe/main.go index 99e7af5..8782f09 100644 --- a/examples/pipe/main.go +++ b/examples/pipe/main.go @@ -11,7 +11,7 @@ func main() { // Example: integers – computing a sum c := collection.New([]int{1, 2, 3}) - sum := c.Pipe(func(col *collection.Collection[int]) any { + sum := collection.Pipe(c, func(col *collection.Collection[int]) int { total := 0 for _, v := range col.Items() { total += v @@ -23,7 +23,7 @@ func main() { // Example: strings – joining values c2 := collection.New([]string{"a", "b", "c"}) - joined := c2.Pipe(func(col *collection.Collection[string]) any { + joined := collection.Pipe(c2, func(col *collection.Collection[string]) string { out := "" for _, v := range col.Items() { out += v @@ -44,7 +44,7 @@ func main() { {ID: 2, Name: "Bob"}, }) - names := users.Pipe(func(col *collection.Collection[User]) any { + 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) diff --git a/examples/pluck/main.go b/examples/pluck/main.go deleted file mode 100644 index 2d18368..0000000 --- a/examples/pluck/main.go +++ /dev/null @@ -1,61 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import "github.com/goforj/collection" - -func main() { - // Pluck is an alias for MapTo with a more semantic name when projecting fields. - // It extracts a single field or computed value from every element and returns a - // new typed collection. - - // Example: integers - extract parity label - nums := collection.New([]int{1, 2, 3, 4}) - parity := collection.Pluck(nums, func(n int) string { - if n%2 == 0 { - return "even" - } - return "odd" - }) - collection.Dump(parity.Items()) - // #[]string [ - // 0 => "odd" #string - // 1 => "even" #string - // 2 => "odd" #string - // 3 => "even" #string - // ] - - // Example: strings - length of each value - words := collection.New([]string{"go", "forj", "rocks"}) - lengths := collection.Pluck(words, func(s string) int { - return len(s) - }) - collection.Dump(lengths.Items()) - // #[]int [ - // 0 => 2 #int - // 1 => 4 #int - // 2 => 5 #int - // ] - - // Example: structs - pluck a field - type User struct { - ID int - Name string - } - - users := collection.New([]User{ - {ID: 1, Name: "Alice"}, - {ID: 2, Name: "Bob"}, - }) - - names := collection.Pluck(users, func(u User) string { - return u.Name - }) - - collection.Dump(names.Items()) - // #[]string [ - // 0 => "Alice" #string - // 1 => "Bob" #string - // ] -} diff --git a/examples/pop/main.go b/examples/pop/main.go index c7a2b9b..a3b2134 100644 --- a/examples/pop/main.go +++ b/examples/pop/main.go @@ -6,14 +6,14 @@ package main import "github.com/goforj/collection" func main() { - // Pop returns the last item and a new collection with that item removed. - // The original collection remains unchanged. + // Pop removes and returns the last item in the collection. // Example: integers c := collection.New([]int{1, 2, 3}) - item, rest := c.Pop() - collection.Dump(item, rest.Items()) + item, ok := c.Pop() + collection.Dump(item, ok, c.Items()) // 3 #int + // true #bool // #[]int [ // 0 => 1 #int // 1 => 2 #int @@ -21,9 +21,10 @@ func main() { // Example: strings c2 := collection.New([]string{"a", "b", "c"}) - item2, rest2 := c2.Pop() - collection.Dump(item2, rest2.Items()) + item2, ok2 := c2.Pop() + collection.Dump(item2, ok2, c2.Items()) // "c" #string + // true #bool // #[]string [ // 0 => "a" #string // 1 => "b" #string @@ -40,12 +41,13 @@ func main() { {ID: 2, Name: "Bob"}, }) - item3, rest3 := users.Pop() - collection.Dump(item3, rest3.Items()) + 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 @@ -55,9 +57,10 @@ func main() { // Example: empty collection empty := collection.New([]int{}) - item4, rest4 := empty.Pop() - collection.Dump(item4, rest4.Items()) + item4, ok4 := empty.Pop() + collection.Dump(item4, ok4, empty.Items()) // 0 #int + // false #bool // #[]int [ // ] } diff --git a/examples/popn/main.go b/examples/popn/main.go index 12b2eaa..a0fe99f 100644 --- a/examples/popn/main.go +++ b/examples/popn/main.go @@ -6,16 +6,15 @@ package main import "github.com/goforj/collection" func main() { - // PopN removes and returns the last n items as a new collection, - // and returns a second collection containing the remaining items. + // PopN removes and returns the last n items in original order. // Example: integers – pop 2 c := collection.New([]int{1, 2, 3, 4}) - popped, rest := c.PopN(2) - collection.Dump(popped.Items(), rest.Items()) + popped := c.PopN(2) + collection.Dump(popped, c.Items()) // #[]int [ - // 0 => 4 #int - // 1 => 3 #int + // 0 => 3 #int + // 1 => 4 #int // ] // #[]int [ // 0 => 1 #int @@ -24,8 +23,8 @@ func main() { // Example: strings – pop 1 c2 := collection.New([]string{"a", "b", "c"}) - popped2, rest2 := c2.PopN(1) - collection.Dump(popped2.Items(), rest2.Items()) + popped2 := c2.PopN(1) + collection.Dump(popped2, c2.Items()) // #[]string [ // 0 => "c" #string // ] @@ -46,17 +45,17 @@ func main() { {ID: 3, Name: "Carol"}, }) - popped3, rest3 := users.PopN(2) - collection.Dump(popped3.Items(), rest3.Items()) + popped3 := users.PopN(2) + collection.Dump(popped3, users.Items()) // #[]main.User [ // 0 => #main.User { - // +ID => 3 #int - // +Name => "Carol" #string - // } - // 1 => #main.User { // +ID => 2 #int // +Name => "Bob" #string // } + // 1 => #main.User { + // +ID => 3 #int + // +Name => "Carol" #string + // } // ] // #[]main.User [ // 0 => #main.User { @@ -65,12 +64,11 @@ func main() { // } // ] - // Example: integers - n <= 0 → returns empty popped + original collection + // Example: integers - n <= 0 → returns nil, no change c3 := collection.New([]int{1, 2, 3}) - popped4, rest4 := c3.PopN(0) - collection.Dump(popped4.Items(), rest4.Items()) - // #[]int [ - // ] + popped4 := c3.PopN(0) + collection.Dump(popped4, c3.Items()) + // // #[]int [ // 0 => 1 #int // 1 => 2 #int @@ -79,11 +77,11 @@ func main() { // Example: strings - n exceeds length → all items popped, rest empty c4 := collection.New([]string{"x", "y"}) - popped5, rest5 := c4.PopN(10) - collection.Dump(popped5.Items(), rest5.Items()) + popped5 := c4.PopN(10) + collection.Dump(popped5, c4.Items()) // #[]string [ - // 0 => "y" #string - // 1 => "x" #string + // 0 => "x" #string + // 1 => "y" #string // ] // #[]string [ // ] diff --git a/examples/prepend/main.go b/examples/prepend/main.go index 9ed4620..d1926b6 100644 --- a/examples/prepend/main.go +++ b/examples/prepend/main.go @@ -6,13 +6,12 @@ package main import "github.com/goforj/collection" func main() { - // Prepend returns a new collection with the given values added - // to the *beginning* of the collection. + // Prepend adds the given values to the beginning of the collection. // Example: integers c := collection.New([]int{3, 4}) - newC := c.Prepend(1, 2) - collection.Dump(newC.Items()) + c.Prepend(1, 2) + collection.Dump(c.Items()) // #[]int [ // 0 => 1 #int // 1 => 2 #int @@ -22,8 +21,8 @@ func main() { // Example: strings letters := collection.New([]string{"c", "d"}) - out := letters.Prepend("a", "b") - collection.Dump(out.Items()) + letters.Prepend("a", "b") + collection.Dump(letters.Items()) // #[]string [ // 0 => "a" #string // 1 => "b" #string @@ -41,8 +40,8 @@ func main() { {ID: 2, Name: "Bob"}, }) - out2 := users.Prepend(User{ID: 1, Name: "Alice"}) - collection.Dump(out2.Items()) + users.Prepend(User{ID: 1, Name: "Alice"}) + collection.Dump(users.Items()) // #[]main.User [ // 0 => #main.User { // +ID => 1 #int @@ -56,17 +55,17 @@ func main() { // Example: integers - Prepending into an empty collection empty := collection.New([]int{}) - out3 := empty.Prepend(9, 8) - collection.Dump(out3.Items()) + empty.Prepend(9, 8) + collection.Dump(empty.Items()) // #[]int [ // 0 => 9 #int // 1 => 8 #int // ] - // Example: integers - Prepending no values → returns a copy of original + // Example: integers - Prepending no values → no change c2 := collection.New([]int{1, 2}) - out4 := c2.Prepend() - collection.Dump(out4.Items()) + c2.Prepend() + collection.Dump(c2.Items()) // #[]int [ // 0 => 1 #int // 1 => 2 #int diff --git a/examples/push/main.go b/examples/push/main.go deleted file mode 100644 index b92656c..0000000 --- a/examples/push/main.go +++ /dev/null @@ -1,53 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import "github.com/goforj/collection" - -func main() { - // Push returns a new collection with the given values appended. - - // Example: integers - nums := collection.New([]int{1, 2}).Push(3, 4) - nums.Dump() - // #[]int [ - // 0 => 1 #int - // 1 => 2 #int - // 2 => 3 #int - // 3 => 4 #int - // ] - - // Complex type (structs) - type User struct { - Name string - Age int - } - - users := collection.New([]User{ - {Name: "Alice", Age: 30}, - {Name: "Bob", Age: 25}, - }).Push( - User{Name: "Carol", Age: 40}, - User{Name: "Dave", Age: 20}, - ) - users.Dump() - // #[]main.User [ - // 0 => #main.User { - // +Name => "Alice" #string - // +Age => 30 #int - // } - // 1 => #main.User { - // +Name => "Bob" #string - // +Age => 25 #int - // } - // 2 => #main.User { - // +Name => "Carol" #string - // +Age => 40 #int - // } - // 3 => #main.User { - // +Name => "Dave" #string - // +Age => 20 #int - // } - // ] -} diff --git a/examples/shuffle/main.go b/examples/shuffle/main.go index 846983a..90f4da6 100644 --- a/examples/shuffle/main.go +++ b/examples/shuffle/main.go @@ -6,21 +6,20 @@ package main import "github.com/goforj/collection" func main() { - // Shuffle randomly shuffles the items in the collection in place - // and returns the same collection for chaining. + // Shuffle returns a shuffled copy of the collection. // Example: integers c := collection.New([]int{1, 2, 3, 4, 5}) - c.Shuffle() - collection.Dump(c.Items()) + out1 := c.Shuffle() + collection.Dump(out1.Items()) // Example: strings – chaining - out := collection.New([]string{"a", "b", "c"}). + out2 := collection.New([]string{"a", "b", "c"}). Shuffle(). Append("d"). Items() - collection.Dump(out) + collection.Dump(out2) // Example: structs type User struct { @@ -34,6 +33,6 @@ func main() { {ID: 4}, }) - users.Shuffle() - collection.Dump(users.Items()) + out3 := users.Shuffle() + collection.Dump(out3.Items()) } diff --git a/examples/take/main.go b/examples/take/main.go index ab2a928..dd9511b 100644 --- a/examples/take/main.go +++ b/examples/take/main.go @@ -14,6 +14,8 @@ func main() { // If n == 0, an empty collection is returned. // // Mirrors Laravel's take() semantics. + // + // 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}) diff --git a/examples/takelast/main.go b/examples/takelast/main.go index c52d6ea..b298e65 100644 --- a/examples/takelast/main.go +++ b/examples/takelast/main.go @@ -14,6 +14,8 @@ func main() { // // 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}) diff --git a/examples/where/main.go b/examples/where/main.go deleted file mode 100644 index 0f82e91..0000000 --- a/examples/where/main.go +++ /dev/null @@ -1,51 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import "github.com/goforj/collection" - -func main() { - // Where keeps only the elements for which fn returns true. - // This is an alias for Filter(fn) for SQL-style ergonomics. - // This method mutates the collection in place and returns the same instance. - - // Example: integers - nums := collection.New([]int{1, 2, 3, 4}) - nums.Where(func(v int) bool { - return v%2 == 0 - }) - collection.Dump(nums.Items()) - // #[]int [ - // 0 => 2 #int - // 1 => 4 #int - // ] - - // 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"}, - }) - - users.Where(func(u User) bool { - return u.ID >= 2 - }) - - collection.Dump(users.Items()) - // #[]main.User [ - // 0 => #main.User { - // +ID => 2 #int - // +Name => "Bob" #string - // } - // 1 => #main.User { - // +ID => 3 #int - // +Name => "Carol" #string - // } - // ] -} diff --git a/find_where.go b/find_where.go deleted file mode 100644 index 7d4bcca..0000000 --- a/find_where.go +++ /dev/null @@ -1,64 +0,0 @@ -package collection - -// FindWhere returns the first item in the collection for which the provided -// predicate function returns true. This is an alias for FirstWhere(fn) and -// exists for ergonomic parity with functional languages (JavaScript, Rust, -// C#, Python) where developers expect a “find” helper. -// @group Querying -// @behavior readonly -// @fluent true -// -// Example: integers -// -// nums := collection.New([]int{1, 2, 3, 4, 5}) -// -// v1, ok1 := nums.FindWhere(func(n int) bool { -// return n == 3 -// }) -// collection.Dump(v1, ok1) -// // 3 #int -// // true #bool -// -// Example: no match -// -// v2, ok2 := nums.FindWhere(func(n int) bool { -// return n > 10 -// }) -// collection.Dump(v2, ok2) -// // 0 #int -// // false #bool -// -// Example: structs -// -// type User struct { -// ID int -// Name string -// } -// -// users := collection.New([]User{ -// {ID: 1, Name: "Alice"}, -// {ID: 2, Name: "Bob"}, -// {ID: 3, Name: "Charlie"}, -// }) -// -// u, ok3 := users.FindWhere(func(u User) bool { -// return u.ID == 2 -// }) -// collection.Dump(u, ok3) -// // #collection.User { -// // +ID => 2 #int -// // +Name => "Bob" #string -// // } -// // true #bool -// -// Example: integers - empty collection -// -// empty := collection.New([]int{}) -// -// v4, ok4 := empty.FindWhere(func(n int) bool { return n == 1 }) -// collection.Dump(v4, ok4) -// // 0 #int -// // false #bool -func (c *Collection[T]) FindWhere(fn func(T) bool) (T, bool) { - return c.FirstWhere(fn) -} diff --git a/find_where_test.go b/find_where_test.go deleted file mode 100644 index 216ee1b..0000000 --- a/find_where_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package collection - -import ( - "reflect" - "testing" -) - -func TestFindWhere_FindsMatchingValue(t *testing.T) { - c := New([]int{1, 2, 3, 4, 5}) - - v, ok := c.FindWhere(func(n int) bool { return n%2 == 0 }) - - if !ok { - t.Fatalf("FindWhere should return ok=true when match exists") - } - - if v != 2 { - t.Fatalf("FindWhere returned wrong value. expected 2, got %v", v) - } -} - -func TestFindWhere_NoMatchReturnsZeroValue(t *testing.T) { - c := New([]int{1, 3, 5}) - - v, ok := c.FindWhere(func(n int) bool { return n%2 == 0 }) - - if ok { - t.Fatalf("FindWhere should return ok=false when no match exists") - } - - if v != 0 { - t.Fatalf("FindWhere should return zero-value when no match exists; got %v", v) - } -} - -func TestFindWhere_AliasToFirst(t *testing.T) { - c := New([]int{1, 2, 3, 4, 5}) - - v1, ok1 := c.FirstWhere(func(n int) bool { return n > 3 }) - v2, ok2 := c.FindWhere(func(n int) bool { return n > 3 }) - - if v1 != v2 || ok1 != ok2 { - t.Fatalf("FindWhere should behave exactly the same as FirstWhere(fn)") - } -} - -func TestFindWhere_WorksWithStructs(t *testing.T) { - type User struct { - ID int - Name string - } - - c := New([]User{ - {ID: 1, Name: "A"}, - {ID: 2, Name: "B"}, - {ID: 3, Name: "C"}, - }) - - expected := User{ID: 2, Name: "B"} - - v, ok := c.FindWhere(func(u User) bool { - return u.ID == 2 - }) - - if !ok { - t.Fatalf("FindWhere should locate struct matching predicate") - } - - if !reflect.DeepEqual(v, expected) { - t.Fatalf("FindWhere returned wrong struct. expected %#v, got %#v", expected, v) - } -} - -func TestFindWhere_NoStructMatch(t *testing.T) { - type User struct { - ID int - Name string - } - - c := New([]User{ - {ID: 1, Name: "A"}, - {ID: 2, Name: "B"}, - }) - - v, ok := c.FindWhere(func(u User) bool { - return u.ID == 999 - }) - - if ok { - t.Fatalf("FindWhere should return ok=false when no struct matches") - } - - if (v != User{}) { - t.Fatalf("FindWhere should return zero-value struct when no match") - } -} diff --git a/from_map.go b/from_map.go index a414572..22f75aa 100644 --- a/from_map.go +++ b/from_map.go @@ -78,5 +78,5 @@ func FromMap[K comparable, V any](m map[K]V) *Collection[Pair[K, V]] { Value: v, }) } - return &Collection[Pair[K, V]]{items: items} + return Attach(items) } diff --git a/intersect.go b/intersect.go index 8367544..74157f1 100644 --- a/intersect.go +++ b/intersect.go @@ -68,7 +68,7 @@ package collection // // ] func Intersect[T comparable](a, b *Collection[T]) *Collection[T] { if len(a.items) == 0 || len(b.items) == 0 { - return New([]T{}) + return Attach([]T{}) } seen := make(map[T]struct{}) @@ -83,5 +83,5 @@ func Intersect[T comparable](a, b *Collection[T]) *Collection[T] { } } - return &Collection[T]{items: out} + return Attach(out) } diff --git a/map.go b/map.go index 1320a20..3683309 100644 --- a/map.go +++ b/map.go @@ -66,8 +66,9 @@ package collection // // } // // ] func (c *Collection[T]) Map(fn func(T) T) *Collection[T] { - for i := range c.items { - c.items[i] = fn(c.items[i]) + out := make([]T, len(c.items)) + for i, v := range c.items { + out[i] = fn(v) } - return c + return Attach(out) } diff --git a/map_test.go b/map_test.go index af65df5..da2d4dc 100644 --- a/map_test.go +++ b/map_test.go @@ -17,6 +17,10 @@ func TestMap_Ints(t *testing.T) { if !reflect.DeepEqual(mapped.items, expected) { t.Fatalf("expected %v, got %v", expected, mapped.items) } + + if !reflect.DeepEqual(c.Items(), []int{1, 2, 3}) { + t.Fatalf("Map should not mutate original collection") + } } func TestMap_Structs(t *testing.T) { @@ -43,6 +47,10 @@ func TestMap_Structs(t *testing.T) { if !reflect.DeepEqual(mapped.items, expected) { t.Fatalf("expected %v, got %v", expected, mapped.items) } + + if !reflect.DeepEqual(c.Items(), []User{{1, "Chris"}, {2, "Van"}}) { + t.Fatalf("Map should not mutate original collection") + } } func TestMap_Empty(t *testing.T) { diff --git a/map_to_test.go b/map_to_test.go new file mode 100644 index 0000000..8685deb --- /dev/null +++ b/map_to_test.go @@ -0,0 +1,72 @@ +package collection + +import ( + "reflect" + "testing" +) + +func TestMapTo_Ints(t *testing.T) { + c := New([]int{1, 2, 3}) + + out := MapTo(c, func(v int) string { + if v%2 == 0 { + return "even" + } + return "odd" + }) + + expected := []string{"odd", "even", "odd"} + if !reflect.DeepEqual(out.Items(), expected) { + t.Fatalf("expected %v, got %v", expected, out.Items()) + } +} + +func TestMapTo_Strings(t *testing.T) { + c := New([]string{"go", "forj", "rocks"}) + + out := MapTo(c, func(s string) int { return len(s) }) + + expected := []int{2, 4, 5} + if !reflect.DeepEqual(out.Items(), expected) { + t.Fatalf("expected %v, got %v", expected, out.Items()) + } +} + +func TestMapTo_Structs(t *testing.T) { + type User struct { + ID int + Name string + } + + users := New([]User{ + {ID: 1, Name: "Alice"}, + {ID: 2, Name: "Bob"}, + }) + + out := MapTo(users, func(u User) string { return u.Name }) + + expected := []string{"Alice", "Bob"} + if !reflect.DeepEqual(out.Items(), expected) { + t.Fatalf("expected %v, got %v", expected, out.Items()) + } +} + +func TestMapTo_Empty(t *testing.T) { + c := New([]int{}) + + out := MapTo(c, func(v int) int { return v * 2 }) + + if len(out.Items()) != 0 { + t.Fatalf("expected empty slice, got %v", out.Items()) + } +} + +func TestMapTo_NoMutation(t *testing.T) { + c := New([]int{1, 2, 3}) + + _ = MapTo(c, func(v int) int { return v * 10 }) + + if !reflect.DeepEqual(c.Items(), []int{1, 2, 3}) { + t.Fatalf("MapTo should not mutate original collection") + } +} diff --git a/merge.go b/merge.go index 8717ad0..386e0ea 100644 --- a/merge.go +++ b/merge.go @@ -1,8 +1,8 @@ package collection -// Merge merges the given data into the current collection. +// Merge merges the given data into a new collection. // @group Transformation -// @behavior mutable +// @behavior immutable // @fluent true // // Example: integers - merging slices @@ -97,7 +97,7 @@ func (c *Collection[T]) mergeSlice(values []T) *Collection[T] { out := make([]T, len(c.items)+len(values)) copy(out, c.items) copy(out[len(c.items):], values) - return &Collection[T]{items: out} + return Attach(out) } /* @@ -150,7 +150,7 @@ func (c *Collection[T]) mergeMap(values map[string]T) *Collection[T] { } // IMPORTANT: return without copying out again - return &Collection[T]{items: out} + return Attach(out) } // fastParseInt is much lighter than strconv.Atoi diff --git a/merge_test.go b/merge_test.go index 8b38b5d..e2ea291 100644 --- a/merge_test.go +++ b/merge_test.go @@ -15,6 +15,10 @@ func TestMerge_AppendsSlice(t *testing.T) { if !reflect.DeepEqual(got, want) { t.Fatalf("Merge([]T) append failed.\nwant=%v\ngot=%v", want, got) } + + if !reflect.DeepEqual(c.Items(), []string{"Desk", "Chair"}) { + t.Fatalf("Merge should not mutate original collection") + } } func TestMerge_AppendsCollection(t *testing.T) { diff --git a/multiply.go b/multiply.go index 4382df0..71cdde4 100644 --- a/multiply.go +++ b/multiply.go @@ -3,7 +3,7 @@ package collection // Multiply creates `n` copies of all items in the collection // and returns a new collection. // @group Transformation -// @behavior mutable +// @behavior immutable // @fluent true // // Example: integers @@ -64,7 +64,7 @@ package collection // // ] func (c *Collection[T]) Multiply(n int) *Collection[T] { if n <= 0 { - return New([]T{}) + return Attach([]T{}) } orig := c.items @@ -74,5 +74,5 @@ func (c *Collection[T]) Multiply(n int) *Collection[T] { out = append(out, orig...) } - return New(out) + return Attach(out) } diff --git a/partition.go b/partition.go index 4107edb..918eea2 100644 --- a/partition.go +++ b/partition.go @@ -76,7 +76,7 @@ package collection // // ] func (c *Collection[T]) Partition(fn func(T) bool) (*Collection[T], *Collection[T]) { if len(c.items) == 0 { - return New([]T{}), New([]T{}) + return Attach([]T{}), Attach([]T{}) } // Pre-size to total length; each partition will shrink to its own length. @@ -91,5 +91,5 @@ func (c *Collection[T]) Partition(fn func(T) bool) (*Collection[T], *Collection[ } } - return New(left), New(right) + return Attach(left), Attach(right) } diff --git a/pipe.go b/pipe.go index 7590c49..6d87010 100644 --- a/pipe.go +++ b/pipe.go @@ -6,13 +6,12 @@ package collection // @behavior readonly // @fluent true // -// This is useful for inline transformations, aggregations, -// or "exiting" a chain with a non-collection value. +// This is a typed escape hatch for branching into arbitrary logic without `any`. // // Example: integers – computing a sum // // c := collection.New([]int{1, 2, 3}) -// sum := c.Pipe(func(col *collection.Collection[int]) any { +// sum := collection.Pipe(c, func(col *collection.Collection[int]) int { // total := 0 // for _, v := range col.Items() { // total += v @@ -25,7 +24,7 @@ package collection // Example: strings – joining values // // c2 := collection.New([]string{"a", "b", "c"}) -// joined := c2.Pipe(func(col *collection.Collection[string]) any { +// joined := collection.Pipe(c2, func(col *collection.Collection[string]) string { // out := "" // for _, v := range col.Items() { // out += v @@ -47,7 +46,7 @@ package collection // {ID: 2, Name: "Bob"}, // }) // -// names := users.Pipe(func(col *collection.Collection[User]) any { +// 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) @@ -60,6 +59,6 @@ package collection // // 0 => "Alice" #string // // 1 => "Bob" #string // // ] -func (c *Collection[T]) Pipe(fn func(*Collection[T]) any) any { +func Pipe[T any, R any](c *Collection[T], fn func(*Collection[T]) R) R { return fn(c) } diff --git a/pipe_test.go b/pipe_test.go index 521ca72..3a78f53 100644 --- a/pipe_test.go +++ b/pipe_test.go @@ -8,7 +8,7 @@ import ( func TestPipe_ReturnsTransformedValue(t *testing.T) { c := New([]int{1, 2, 3}) - result := c.Pipe(func(col *Collection[int]) any { + result := Pipe(c, func(col *Collection[int]) int { sum := 0 for _, v := range col.Items() { sum += v @@ -16,7 +16,7 @@ func TestPipe_ReturnsTransformedValue(t *testing.T) { return sum }) - if result.(int) != 6 { + if result != 6 { t.Fatalf("Pipe() expected sum=6, got=%v", result) } } @@ -24,11 +24,11 @@ func TestPipe_ReturnsTransformedValue(t *testing.T) { func TestPipe_CanReturnCollection(t *testing.T) { c := New([]int{1, 2, 3}) - result := c.Pipe(func(col *Collection[int]) any { + result := Pipe(c, func(col *Collection[int]) *Collection[int] { return col.Filter(func(v int) bool { return v > 1 }) }) - out := result.(*Collection[int]).Items() + out := result.Items() want := []int{2, 3} if !reflect.DeepEqual(out, want) { @@ -39,9 +39,13 @@ func TestPipe_CanReturnCollection(t *testing.T) { func TestPipe_IsNonMutating(t *testing.T) { c := New([]int{1, 2, 3}) - _ = c.Pipe(func(col *Collection[int]) any { + _ = Pipe(c, func(col *Collection[int]) *Collection[int] { return col.Map(func(v int) int { return v * 2 }) }) + + if !reflect.DeepEqual(c.Items(), []int{1, 2, 3}) { + t.Fatalf("Pipe() should not mutate original collection") + } } func TestPipe_ReceivesCorrectCollection(t *testing.T) { @@ -49,9 +53,9 @@ func TestPipe_ReceivesCorrectCollection(t *testing.T) { calledWith := "" - c.Pipe(func(col *Collection[string]) any { + Pipe(c, func(col *Collection[string]) string { calledWith = col.Items()[0] - return nil + return "" }) if calledWith != "a" { diff --git a/pluck.go b/pluck.go index 2332fe7..effec25 100644 --- a/pluck.go +++ b/pluck.go @@ -64,67 +64,5 @@ func MapTo[T any, R any](c *Collection[T], fn func(T) R) *Collection[R] { for i, v := range items { out[i] = fn(v) } - return &Collection[R]{items: out} -} - -// Pluck is an alias for MapTo with a more semantic name when projecting fields. -// It extracts a single field or computed value from every element and returns a -// new typed collection. -// @group Transformation -// @behavior immutable -// @fluent true -// -// Example: integers - extract parity label -// -// nums := collection.New([]int{1, 2, 3, 4}) -// parity := collection.Pluck(nums, func(n int) string { -// if n%2 == 0 { -// return "even" -// } -// return "odd" -// }) -// collection.Dump(parity.Items()) -// // #[]string [ -// // 0 => "odd" #string -// // 1 => "even" #string -// // 2 => "odd" #string -// // 3 => "even" #string -// // ] -// -// Example: strings - length of each value -// -// words := collection.New([]string{"go", "forj", "rocks"}) -// lengths := collection.Pluck(words, func(s string) int { -// return len(s) -// }) -// collection.Dump(lengths.Items()) -// // #[]int [ -// // 0 => 2 #int -// // 1 => 4 #int -// // 2 => 5 #int -// // ] -// -// Example: structs - pluck a field -// -// type User struct { -// ID int -// Name string -// } -// -// users := collection.New([]User{ -// {ID: 1, Name: "Alice"}, -// {ID: 2, Name: "Bob"}, -// }) -// -// names := collection.Pluck(users, func(u User) string { -// return u.Name -// }) -// -// collection.Dump(names.Items()) -// // #[]string [ -// // 0 => "Alice" #string -// // 1 => "Bob" #string -// // ] -func Pluck[T any, R any](c *Collection[T], fn func(T) R) *Collection[R] { - return MapTo(c, fn) + return Attach(out) } diff --git a/pluck_test.go b/pluck_test.go deleted file mode 100644 index 6bf4d80..0000000 --- a/pluck_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package collection - -import ( - "reflect" - "testing" -) - -func TestMapTo_Ints(t *testing.T) { - c := New([]int{1, 2, 3}) - - out := MapTo(c, func(v int) int { - return v * v - }) - - expected := []int{1, 4, 9} - - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("expected %v, got %v", expected, out.items) - } -} - -func TestMapTo_ChangeType(t *testing.T) { - c := New([]int{1, 2, 3}) - - out := MapTo(c, func(v int) string { - return "num:" + string(rune('0'+v)) - }) - - expected := []string{"num:1", "num:2", "num:3"} - - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("expected %v, got %v", expected, out.items) - } -} - -func TestMapTo_StructToInt(t *testing.T) { - type User struct { - ID int - Name string - } - - c := New([]User{ - {1, "Chris"}, - {2, "Van"}, - {3, "Shawn"}, - }) - - out := MapTo(c, func(u User) int { - return u.ID - }) - - expected := []int{1, 2, 3} - - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("expected %v, got %v", expected, out.items) - } -} - -func TestPluck_StructField(t *testing.T) { - type User struct { - ID int - Name string - } - - c := New([]User{ - {1, "Chris"}, - {2, "Van"}, - {3, "Shawn"}, - }) - - out := Pluck(c, func(u User) string { - return u.Name - }) - - expected := []string{"Chris", "Van", "Shawn"} - - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("expected %v, got %v", expected, out.items) - } -} - -func TestMapTo_Empty(t *testing.T) { - c := New([]int{}) - - out := MapTo(c, func(v int) int { - return v * 10 - }) - - if len(out.items) != 0 { - t.Fatalf("expected empty result, got %v", out.items) - } -} - -func TestPluck_Empty(t *testing.T) { - type User struct{ Name string } - - c := New([]User{}) - - out := Pluck(c, func(u User) string { - return u.Name - }) - - if len(out.items) != 0 { - t.Fatalf("expected empty result, got %v", out.items) - } -} - -func TestMapTo_NoMutation(t *testing.T) { - c := New([]int{1, 2, 3}) - orig := append([]int{}, c.items...) - - _ = MapTo(c, func(v int) int { - return v * 100 - }) - - if !reflect.DeepEqual(c.items, orig) { - t.Fatalf("MapTo mutated original collection: %v vs %v", c.items, orig) - } -} - -func TestPluck_NoMutation(t *testing.T) { - type User struct { - ID int - Name string - } - - c := New([]User{ - {1, "Chris"}, - {2, "Van"}, - }) - - orig := append([]User{}, c.items...) - - _ = Pluck(c, func(u User) string { return u.Name }) - - if !reflect.DeepEqual(c.items, orig) { - t.Fatalf("Pluck mutated original collection: %v vs %v", c.items, orig) - } -} diff --git a/pop.go b/pop.go index dccb10d..3eeed93 100644 --- a/pop.go +++ b/pop.go @@ -1,20 +1,19 @@ package collection -// Pop returns the last item and a new collection with that item removed. -// The original collection remains unchanged. +// Pop removes and returns the last item in the collection. // @group Slicing // @behavior mutable -// @fluent true +// @fluent false // -// If the collection is empty, the zero value of T is returned along with -// an empty collection. +// If the collection is empty, the zero value of T is returned with ok=false. // // Example: integers // // c := collection.New([]int{1, 2, 3}) -// item, rest := c.Pop() -// collection.Dump(item, rest.Items()) +// item, ok := c.Pop() +// collection.Dump(item, ok, c.Items()) // // 3 #int +// // true #bool // // #[]int [ // // 0 => 1 #int // // 1 => 2 #int @@ -23,9 +22,10 @@ package collection // Example: strings // // c2 := collection.New([]string{"a", "b", "c"}) -// item2, rest2 := c2.Pop() -// collection.Dump(item2, rest2.Items()) +// item2, ok2 := c2.Pop() +// collection.Dump(item2, ok2, c2.Items()) // // "c" #string +// // true #bool // // #[]string [ // // 0 => "a" #string // // 1 => "b" #string @@ -43,12 +43,13 @@ package collection // {ID: 2, Name: "Bob"}, // }) // -// item3, rest3 := users.Pop() -// collection.Dump(item3, rest3.Items()) +// 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 @@ -59,42 +60,38 @@ package collection // Example: empty collection // // empty := collection.New([]int{}) -// item4, rest4 := empty.Pop() -// collection.Dump(item4, rest4.Items()) +// item4, ok4 := empty.Pop() +// collection.Dump(item4, ok4, empty.Items()) // // 0 #int +// // false #bool // // #[]int [ // // ] -func (c *Collection[T]) Pop() (T, *Collection[T]) { +func (c *Collection[T]) Pop() (T, bool) { n := len(c.items) if n == 0 { var zero T - return zero, New([]T{}) + return zero, false } item := c.items[n-1] - rest := c.items[:n-1] - - return item, New(rest) + c.items = c.items[:n-1] + return item, true } -// PopN removes and returns the last n items as a new collection, -// and returns a second collection containing the remaining items. +// PopN removes and returns the last n items in original order. // @group Slicing // @behavior mutable -// @fluent true -// -// The popped items are returned in reverse order, matching the behavior -// of repeated Pop() calls. +// @fluent false // // Example: integers – pop 2 // // c := collection.New([]int{1, 2, 3, 4}) -// popped, rest := c.PopN(2) -// collection.Dump(popped.Items(), rest.Items()) +// popped := c.PopN(2) +// collection.Dump(popped, c.Items()) // // #[]int [ -// // 0 => 4 #int -// // 1 => 3 #int +// // 0 => 3 #int +// // 1 => 4 #int // // ] // // #[]int [ // // 0 => 1 #int @@ -104,8 +101,8 @@ func (c *Collection[T]) Pop() (T, *Collection[T]) { // Example: strings – pop 1 // // c2 := collection.New([]string{"a", "b", "c"}) -// popped2, rest2 := c2.PopN(1) -// collection.Dump(popped2.Items(), rest2.Items()) +// popped2 := c2.PopN(1) +// collection.Dump(popped2, c2.Items()) // // #[]string [ // // 0 => "c" #string // // ] @@ -127,17 +124,17 @@ func (c *Collection[T]) Pop() (T, *Collection[T]) { // {ID: 3, Name: "Carol"}, // }) // -// popped3, rest3 := users.PopN(2) -// collection.Dump(popped3.Items(), rest3.Items()) +// popped3 := users.PopN(2) +// collection.Dump(popped3, users.Items()) // // #[]main.User [ // // 0 => #main.User { -// // +ID => 3 #int -// // +Name => "Carol" #string -// // } -// // 1 => #main.User { // // +ID => 2 #int // // +Name => "Bob" #string // // } +// // 1 => #main.User { +// // +ID => 3 #int +// // +Name => "Carol" #string +// // } // // ] // // #[]main.User [ // // 0 => #main.User { @@ -146,13 +143,12 @@ func (c *Collection[T]) Pop() (T, *Collection[T]) { // // } // // ] // -// Example: integers - n <= 0 → returns empty popped + original collection +// Example: integers - n <= 0 → returns nil, no change // // c3 := collection.New([]int{1, 2, 3}) -// popped4, rest4 := c3.PopN(0) -// collection.Dump(popped4.Items(), rest4.Items()) -// // #[]int [ -// // ] +// popped4 := c3.PopN(0) +// collection.Dump(popped4, c3.Items()) +// // // // #[]int [ // // 0 => 1 #int // // 1 => 2 #int @@ -162,35 +158,25 @@ func (c *Collection[T]) Pop() (T, *Collection[T]) { // Example: strings - n exceeds length → all items popped, rest empty // // c4 := collection.New([]string{"x", "y"}) -// popped5, rest5 := c4.PopN(10) -// collection.Dump(popped5.Items(), rest5.Items()) +// popped5 := c4.PopN(10) +// collection.Dump(popped5, c4.Items()) // // #[]string [ -// // 0 => "y" #string -// // 1 => "x" #string +// // 0 => "x" #string +// // 1 => "y" #string // // ] // // #[]string [ // // ] -func (c *Collection[T]) PopN(n int) (*Collection[T], *Collection[T]) { +func (c *Collection[T]) PopN(n int) []T { if n <= 0 || len(c.items) == 0 { - return New([]T{}), c + return nil } total := len(c.items) - - if n >= total { - return New(reverseCopy(c.items)), New([]T{}) + if n > total { + n = total } - remain := c.items[:total-n] popped := c.items[total-n:] - - return New(reverseCopy(popped)), New(remain) -} - -func reverseCopy[T any](src []T) []T { - out := make([]T, len(src)) - for i := range src { - out[i] = src[len(src)-1-i] - } - return out + c.items = c.items[:total-n] + return popped } diff --git a/pop_test.go b/pop_test.go index 5d4816d..e22855a 100644 --- a/pop_test.go +++ b/pop_test.go @@ -8,101 +8,89 @@ import ( func TestPop_RemovesOne(t *testing.T) { c := New([]int{1, 2, 3}) - v, c2 := c.Pop() + v, ok := c.Pop() if v != 3 { t.Fatalf("Pop() expected 3, got %v", v) } - want := []int{1, 2} - if !reflect.DeepEqual(c2.Items(), want) { - t.Fatalf("Pop() expected remainder %v, got %v", want, c2.Items()) + if !ok { + t.Fatalf("Pop() expected ok=true") } - // original must remain unchanged - if !reflect.DeepEqual(c.Items(), []int{1, 2, 3}) { - t.Fatalf("Pop() mutated original collection") + want := []int{1, 2} + if !reflect.DeepEqual(c.Items(), want) { + t.Fatalf("Pop() expected remainder %v, got %v", want, c.Items()) } } func TestPop_OnEmptyReturnsZero(t *testing.T) { c := New([]int{}) - v, c2 := c.Pop() + v, ok := c.Pop() if v != 0 { t.Fatalf("Pop() on empty should return zero-value, got %v", v) } - if len(c2.Items()) != 0 { - t.Fatalf("Pop() on empty should return empty collection") + if ok { + t.Fatalf("Pop() on empty should return ok=false") + } + + if len(c.Items()) != 0 { + t.Fatalf("Pop() on empty should keep collection empty") } } func TestPopN_RemovesMultiple(t *testing.T) { c := New([]int{1, 2, 3, 4, 5}) - popped, remain := c.PopN(3) + popped := c.PopN(3) - wantPopped := []int{5, 4, 3} + wantPopped := []int{3, 4, 5} wantRemain := []int{1, 2} - if !reflect.DeepEqual(popped.Items(), wantPopped) { - t.Fatalf("PopN(3) wrong popped values. want=%v got=%v", wantPopped, popped.Items()) - } - - if !reflect.DeepEqual(remain.Items(), wantRemain) { - t.Fatalf("PopN(3) wrong remaining values. want=%v got=%v", wantRemain, remain.Items()) + if !reflect.DeepEqual(popped, wantPopped) { + t.Fatalf("PopN(3) wrong popped values. want=%v got=%v", wantPopped, popped) } - // original must remain unchanged - if !reflect.DeepEqual(c.Items(), []int{1, 2, 3, 4, 5}) { - t.Fatalf("PopN() mutated original collection") + if !reflect.DeepEqual(c.Items(), wantRemain) { + t.Fatalf("PopN(3) wrong remaining values. want=%v got=%v", wantRemain, c.Items()) } } func TestPopN_MoreThanLength(t *testing.T) { c := New([]int{1, 2}) - popped, remain := c.PopN(10) + popped := c.PopN(10) - wantPopped := []int{2, 1} + wantPopped := []int{1, 2} - if !reflect.DeepEqual(popped.Items(), wantPopped) { - t.Fatalf("PopN(>len) wrong popped. want=%v got=%v", wantPopped, popped.Items()) + if !reflect.DeepEqual(popped, wantPopped) { + t.Fatalf("PopN(>len) wrong popped. want=%v got=%v", wantPopped, popped) } - if len(remain.Items()) != 0 { - t.Fatalf("PopN(>len) should return empty remainder") - } - - // original unchanged - if !reflect.DeepEqual(c.Items(), []int{1, 2}) { - t.Fatalf("PopN() mutated original collection") + if len(c.Items()) != 0 { + t.Fatalf("PopN(>len) should leave empty remainder") } } func TestPopN_ZeroOrNegative(t *testing.T) { c := New([]int{1, 2, 3}) - popped, remain := c.PopN(0) - if len(popped.Items()) != 0 { - t.Fatalf("PopN(0) should return empty popped collection") + popped := c.PopN(0) + if popped != nil { + t.Fatalf("PopN(0) should return nil") } - if !reflect.DeepEqual(remain.Items(), []int{1, 2, 3}) { + if !reflect.DeepEqual(c.Items(), []int{1, 2, 3}) { t.Fatalf("PopN(0) should not modify original") } - popped, remain = c.PopN(-5) - if len(popped.Items()) != 0 { - t.Fatalf("PopN(-n) should return empty collection") + popped = c.PopN(-5) + if popped != nil { + t.Fatalf("PopN(-n) should return nil") } - if !reflect.DeepEqual(remain.Items(), []int{1, 2, 3}) { - t.Fatalf("PopN(-n) should not modify original") - } - - // original still unchanged if !reflect.DeepEqual(c.Items(), []int{1, 2, 3}) { - t.Fatalf("PopN zero/negative mutated original collection") + t.Fatalf("PopN(-n) should not modify original") } } diff --git a/prepend.go b/prepend.go index 7edb684..5338fc5 100644 --- a/prepend.go +++ b/prepend.go @@ -1,18 +1,17 @@ package collection -// Prepend returns a new collection with the given values added -// to the *beginning* of the collection. +// Prepend adds the given values to the beginning of the collection. // @group Transformation // @behavior mutable // @fluent true // -// The original collection is not modified. +// This method mutates the collection in place and returns the same instance. // // Example: integers // // c := collection.New([]int{3, 4}) -// newC := c.Prepend(1, 2) -// collection.Dump(newC.Items()) +// c.Prepend(1, 2) +// collection.Dump(c.Items()) // // #[]int [ // // 0 => 1 #int // // 1 => 2 #int @@ -23,8 +22,8 @@ package collection // Example: strings // // letters := collection.New([]string{"c", "d"}) -// out := letters.Prepend("a", "b") -// collection.Dump(out.Items()) +// letters.Prepend("a", "b") +// collection.Dump(letters.Items()) // // #[]string [ // // 0 => "a" #string // // 1 => "b" #string @@ -43,8 +42,8 @@ package collection // {ID: 2, Name: "Bob"}, // }) // -// out2 := users.Prepend(User{ID: 1, Name: "Alice"}) -// collection.Dump(out2.Items()) +// users.Prepend(User{ID: 1, Name: "Alice"}) +// collection.Dump(users.Items()) // // #[]main.User [ // // 0 => #main.User { // // +ID => 1 #int @@ -59,18 +58,18 @@ package collection // Example: integers - Prepending into an empty collection // // empty := collection.New([]int{}) -// out3 := empty.Prepend(9, 8) -// collection.Dump(out3.Items()) +// empty.Prepend(9, 8) +// collection.Dump(empty.Items()) // // #[]int [ // // 0 => 9 #int // // 1 => 8 #int // // ] // -// Example: integers - Prepending no values → returns a copy of original +// Example: integers - Prepending no values → no change // // c2 := collection.New([]int{1, 2}) -// out4 := c2.Prepend() -// collection.Dump(out4.Items()) +// c2.Prepend() +// collection.Dump(c2.Items()) // // #[]int [ // // 0 => 1 #int // // 1 => 2 #int @@ -79,5 +78,6 @@ func (c *Collection[T]) Prepend(values ...T) *Collection[T] { out := make([]T, 0, len(c.items)+len(values)) out = append(out, values...) out = append(out, c.items...) - return &Collection[T]{items: out} + c.items = out + return c } diff --git a/prepend_test.go b/prepend_test.go index ffe913f..f878334 100644 --- a/prepend_test.go +++ b/prepend_test.go @@ -10,9 +10,13 @@ func TestPrepend_Basic(t *testing.T) { out := c.Prepend(1, 2) + if out != c { + t.Fatalf("expected Prepend to return same collection instance") + } + expected := []int{1, 2, 3, 4} - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("expected %v, got %v", expected, out.items) + if !reflect.DeepEqual(c.items, expected) { + t.Fatalf("expected %v, got %v", expected, c.items) } } @@ -21,9 +25,13 @@ func TestPrepend_EmptyCollection(t *testing.T) { out := c.Prepend(1, 2, 3) + if out != c { + t.Fatalf("expected Prepend to return same collection instance") + } + expected := []int{1, 2, 3} - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("expected %v, got %v", expected, out.items) + if !reflect.DeepEqual(c.items, expected) { + t.Fatalf("expected %v, got %v", expected, c.items) } } @@ -32,9 +40,13 @@ func TestPrepend_NoValues(t *testing.T) { out := c.Prepend() // no-op + if out != c { + t.Fatalf("expected Prepend to return same collection instance") + } + expected := []int{1, 2, 3} - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("expected %v, got %v", expected, out.items) + if !reflect.DeepEqual(c.items, expected) { + t.Fatalf("expected %v, got %v", expected, c.items) } } @@ -51,6 +63,10 @@ func TestPrepend_Structs(t *testing.T) { out := c.Prepend(User{1, "Chris"}, User{2, "Matt"}) + if out != c { + t.Fatalf("expected Prepend to return same collection instance") + } + expected := []User{ {1, "Chris"}, {2, "Matt"}, @@ -58,25 +74,27 @@ func TestPrepend_Structs(t *testing.T) { {4, "Van"}, } - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("expected %v, got %v", expected, out.items) + if !reflect.DeepEqual(c.items, expected) { + t.Fatalf("expected %v, got %v", expected, c.items) } } -func TestPrepend_NoMutation(t *testing.T) { +func TestPrepend_DoesNotMutateSourceSlice(t *testing.T) { orig := []int{10, 20, 30} c := New(orig) out := c.Prepend(5, 7) - // original must remain unchanged - if !reflect.DeepEqual(c.items, orig) { - t.Fatalf("Prepend mutated original collection: %v", c.items) + if out != c { + t.Fatalf("expected Prepend to return same collection instance") + } + + if !reflect.DeepEqual(orig, []int{10, 20, 30}) { + t.Fatalf("Prepend mutated source slice: %v", orig) } - // ensure new collection is different & correct expected := []int{5, 7, 10, 20, 30} - if !reflect.DeepEqual(out.items, expected) { - t.Fatalf("expected %v, got %v", expected, out.items) + if !reflect.DeepEqual(c.items, expected) { + t.Fatalf("expected %v, got %v", expected, c.items) } } diff --git a/reverse_test.go b/reverse_test.go index 98eb50e..753503e 100644 --- a/reverse_test.go +++ b/reverse_test.go @@ -86,7 +86,7 @@ func TestReverse_Chainable(t *testing.T) { func TestReverse_MutatesInPlace(t *testing.T) { items := []int{1, 2, 3} - c := New(items) + c := Attach(items) c.Reverse() diff --git a/shuffle.go b/shuffle.go index 7094855..84085f3 100644 --- a/shuffle.go +++ b/shuffle.go @@ -15,13 +15,12 @@ func setShuffleRand(r *rand.Rand) { shuffleRand = r } -// Shuffle randomly shuffles the items in the collection in place -// and returns the same collection for chaining. +// Shuffle returns a shuffled copy of the collection. // @group Ordering -// @behavior mutable +// @behavior immutable // @fluent true // -// This operation performs no allocations. +// This operation allocates a new slice and does not mutate the receiver. // // The shuffle uses an internal random source. Tests may override // this source to achieve deterministic behavior. @@ -29,17 +28,17 @@ func setShuffleRand(r *rand.Rand) { // Example: integers // // c := collection.New([]int{1, 2, 3, 4, 5}) -// c.Shuffle() -// collection.Dump(c.Items()) +// out1 := c.Shuffle() +// collection.Dump(out1.Items()) // // Example: strings – chaining // -// out := collection.New([]string{"a", "b", "c"}). +// out2 := collection.New([]string{"a", "b", "c"}). // Shuffle(). // Append("d"). // Items() // -// collection.Dump(out) +// collection.Dump(out2) // // Example: structs // @@ -54,17 +53,19 @@ func setShuffleRand(r *rand.Rand) { // {ID: 4}, // }) // -// users.Shuffle() -// collection.Dump(users.Items()) +// out3 := users.Shuffle() +// collection.Dump(out3.Items()) func (c *Collection[T]) Shuffle() *Collection[T] { items := c.items - n := len(items) + out := make([]T, len(items)) + copy(out, items) + n := len(out) - // Fisher–Yates shuffle (in place, zero alloc) + // Fisher–Yates shuffle (in place on the copy) for i := n - 1; i > 0; i-- { j := shuffleRand.Intn(i + 1) - items[i], items[j] = items[j], items[i] + out[i], out[j] = out[j], out[i] } - return c + return Attach(out) } diff --git a/shuffle_test.go b/shuffle_test.go index 93d0eb4..6f85aef 100644 --- a/shuffle_test.go +++ b/shuffle_test.go @@ -20,14 +20,12 @@ func TestShuffle_Deterministic(t *testing.T) { withDeterministicShuffle(t, 42, func() { c := New([]int{1, 2, 3, 4, 5}) - c.Shuffle() - first := append([]int(nil), c.Items()...) // snapshot + first := append([]int(nil), c.Shuffle().Items()...) // snapshot // Shuffle again with the same deterministic RNG reset setShuffleRand(rand.New(rand.NewSource(42))) c2 := New([]int{1, 2, 3, 4, 5}) - c2.Shuffle() - second := c2.Items() + second := c2.Shuffle().Items() if !reflect.DeepEqual(first, second) { t.Fatalf("expected deterministic shuffle within same RNG, got %v vs %v", first, second) @@ -40,8 +38,7 @@ func TestShuffle_PreservesAllElements(t *testing.T) { orig := []int{1, 2, 3, 4, 5} c := New(orig) - c.Shuffle() - out := c.Items() + out := c.Shuffle().Items() if len(out) != len(orig) { t.Fatalf("expected length %d, got %d", len(orig), len(out)) @@ -62,20 +59,20 @@ func TestShuffle_PreservesAllElements(t *testing.T) { func TestShuffle_Empty(t *testing.T) { c := New([]int{}) - c.Shuffle() + out := c.Shuffle() - if len(c.Items()) != 0 { - t.Fatalf("expected empty slice, got %v", c.Items()) + if len(out.Items()) != 0 { + t.Fatalf("expected empty slice, got %v", out.Items()) } } func TestShuffle_SingleElement(t *testing.T) { c := New([]int{42}) - c.Shuffle() + out := c.Shuffle() expect := []int{42} - if !reflect.DeepEqual(c.Items(), expect) { - t.Fatalf("expected %v, got %v", expect, c.Items()) + if !reflect.DeepEqual(out.Items(), expect) { + t.Fatalf("expected %v, got %v", expect, out.Items()) } } @@ -91,10 +88,10 @@ func TestShuffle_Structs(t *testing.T) { {ID: 3}, }) - c.Shuffle() + out := c.Shuffle() ids := map[int]bool{} - for _, u := range c.Items() { + for _, u := range out.Items() { ids[u.ID] = true } @@ -106,15 +103,19 @@ func TestShuffle_Structs(t *testing.T) { }) } -func TestShuffle_MutatesInPlace(t *testing.T) { +func TestShuffle_DoesNotMutateOriginal(t *testing.T) { withDeterministicShuffle(t, 99, func() { items := []int{1, 2, 3, 4} c := New(items) - c.Shuffle() + out := c.Shuffle() - if reflect.DeepEqual(items, []int{1, 2, 3, 4}) { - t.Fatalf("expected underlying slice to be shuffled in place") + if !reflect.DeepEqual(items, []int{1, 2, 3, 4}) { + t.Fatalf("expected source slice to remain unchanged") + } + + if reflect.DeepEqual(out.Items(), []int{1, 2, 3, 4}) { + t.Fatalf("expected shuffled output to differ from original order") } }) } diff --git a/skip.go b/skip.go index 6c5f4ea..0c2509d 100644 --- a/skip.go +++ b/skip.go @@ -11,6 +11,8 @@ package 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}) @@ -67,12 +69,12 @@ func (c *Collection[T]) Skip(n int) *Collection[T] { l := len(items) if n <= 0 { - return &Collection[T]{items: items} + return Attach(items) } if n >= l { - return &Collection[T]{items: items[:0]} + return Attach(items[:0]) } - return &Collection[T]{items: items[n:]} + return Attach(items[n:]) } diff --git a/skip_last.go b/skip_last.go index 250da56..85527a2 100644 --- a/skip_last.go +++ b/skip_last.go @@ -11,6 +11,8 @@ package 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}) @@ -63,12 +65,12 @@ func (c *Collection[T]) SkipLast(n int) *Collection[T] { l := len(items) if n <= 0 { - return &Collection[T]{items: items} + return Attach(items) } if n >= l { - return &Collection[T]{items: items[:0]} + return Attach(items[:0]) } - return &Collection[T]{items: items[:l-n]} + return Attach(items[:l-n]) } diff --git a/skip_last_test.go b/skip_last_test.go index ce1d7c2..f641858 100644 --- a/skip_last_test.go +++ b/skip_last_test.go @@ -94,7 +94,7 @@ func TestSkipLast_DoesNotMutateOriginal(t *testing.T) { func TestSkipLast_ReusesBackingSlice(t *testing.T) { items := []int{1, 2, 3, 4} - c := New(items) + c := Attach(items) out := c.SkipLast(1) out.Items()[0] = 99 diff --git a/skip_test.go b/skip_test.go index a669bb2..6c7b0f5 100644 --- a/skip_test.go +++ b/skip_test.go @@ -94,7 +94,7 @@ func TestSkip_DoesNotMutateOriginal(t *testing.T) { func TestSkip_ReusesBackingSlice(t *testing.T) { items := []int{1, 2, 3, 4} - c := New(items) + c := Attach(items) out := c.Skip(1) diff --git a/symmetric_difference.go b/symmetric_difference.go index 71e7ca3..46f4cd5 100644 --- a/symmetric_difference.go +++ b/symmetric_difference.go @@ -98,5 +98,5 @@ func SymmetricDifference[T comparable](a, b *Collection[T]) *Collection[T] { out = append(out, v) } - return &Collection[T]{items: out} + return Attach(out) } diff --git a/take.go b/take.go index 5ad4ecf..76301d1 100644 --- a/take.go +++ b/take.go @@ -9,6 +9,8 @@ package collection // // Mirrors Laravel's take() semantics. // +// NOTE: returns a view (shares backing array). Use Clone() to detach. +// // @group Slicing // @behavior immutable // Example: integers - take first 3 @@ -54,24 +56,24 @@ func (c *Collection[T]) Take(n int) *Collection[T] { // Empty or zero → empty collection if n == 0 || length == 0 { - return New([]T{}) + return Attach([]T{}) } // Positive → take from start if n > 0 { if n >= length { // no need to allocate; just reuse original - return &Collection[T]{items: c.items} + return Attach(c.items) } - return &Collection[T]{items: c.items[:n]} + return Attach(c.items[:n]) } // Negative → take from end n = -n if n >= length { - return &Collection[T]{items: c.items} + return Attach(c.items) } start := length - n - return &Collection[T]{items: c.items[start:]} + return Attach(c.items[start:]) } diff --git a/take_last.go b/take_last.go index 2ae8656..713a779 100644 --- a/take_last.go +++ b/take_last.go @@ -8,6 +8,8 @@ package collection // // This operation performs no element allocations; it re-slices the // underlying slice. +// +// NOTE: returns a view (shares backing array). Use Clone() to detach. // @group Slicing // @behavior immutable // Example: integers @@ -60,12 +62,12 @@ func (c *Collection[T]) TakeLast(n int) *Collection[T] { l := len(items) if n <= 0 { - return &Collection[T]{items: items[:0]} + return Attach(items[:0]) } if n >= l { - return &Collection[T]{items: items} + return Attach(items) } - return &Collection[T]{items: items[l-n:]} + return Attach(items[l-n:]) } diff --git a/take_last_test.go b/take_last_test.go index 62a5bba..6fc7853 100644 --- a/take_last_test.go +++ b/take_last_test.go @@ -93,7 +93,7 @@ func TestTakeLast_DoesNotMutateOriginal(t *testing.T) { func TestTakeLast_ReusesBackingSlice(t *testing.T) { items := []int{1, 2, 3, 4} - c := New(items) + c := Attach(items) out := c.TakeLast(2) out.Items()[0] = 99 diff --git a/take_until.go b/take_until.go index 684a610..f960de9 100644 --- a/take_until.go +++ b/take_until.go @@ -5,6 +5,8 @@ package collection // @group Slicing // @behavior immutable // @fluent true +// +// 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}) @@ -34,16 +36,15 @@ package collection // // 2 => 3 #int // // ] func (c *Collection[T]) TakeUntilFn(pred func(T) bool) *Collection[T] { - out := make([]T, 0, len(c.items)) - - for _, v := range c.items { + idx := len(c.items) + for i, v := range c.items { if pred(v) { + idx = i break } - out = append(out, v) } - return New(out) + return Attach(c.items[:idx]) } // TakeUntil returns items until the first element equals `value`. @@ -53,6 +54,8 @@ func (c *Collection[T]) TakeUntilFn(pred func(T) bool) *Collection[T] { // Uses == comparison, so T must be comparable. // @group Slicing // @behavior immutable +// +// NOTE: returns a view (shares backing array). Use Clone() to detach. // Example: integers - stop at value 3 // // c4 := collection.New([]int{1, 2, 3, 4}) @@ -82,14 +85,13 @@ func (c *Collection[T]) TakeUntilFn(pred func(T) bool) *Collection[T] { // // #[]int [ // // ] func TakeUntil[T comparable](c *Collection[T], value T) *Collection[T] { - out := make([]T, 0, len(c.items)) - - for _, v := range c.items { + idx := len(c.items) + for i, v := range c.items { if v == value { + idx = i break } - out = append(out, v) } - return New(out) + return Attach(c.items[:idx]) } diff --git a/times.go b/times.go index e1f958b..c5a59a4 100644 --- a/times.go +++ b/times.go @@ -65,7 +65,7 @@ package collection // // ] func Times[T any](count int, fn func(int) T) *Collection[T] { if count <= 0 { - return New([]T{}) + return Attach([]T{}) } out := make([]T, count) @@ -73,5 +73,5 @@ func Times[T any](count int, fn func(int) T) *Collection[T] { out[i-1] = fn(i) } - return New(out) + return Attach(out) } diff --git a/union.go b/union.go index 13458bf..dccc8ed 100644 --- a/union.go +++ b/union.go @@ -88,5 +88,5 @@ func Union[T comparable](a, b *Collection[T]) *Collection[T] { out = append(out, v) } - return &Collection[T]{items: out} + return Attach(out) } diff --git a/unique.go b/unique.go index 127bde5..7194b46 100644 --- a/unique.go +++ b/unique.go @@ -72,5 +72,5 @@ func (c *Collection[T]) Unique(eq func(a, b T) bool) *Collection[T] { } } - return &Collection[T]{items: out} + return Attach(out) } diff --git a/unique_by.go b/unique_by.go index 715076a..1b1e742 100644 --- a/unique_by.go +++ b/unique_by.go @@ -58,7 +58,7 @@ func UniqueBy[T any, K comparable](c *Collection[T], keyFn func(T) K) *Collectio items := c.items n := len(items) if n == 0 { - return New([]T{}) + return Attach([]T{}) } seen := make(map[K]struct{}, n) @@ -73,5 +73,5 @@ func UniqueBy[T any, K comparable](c *Collection[T], keyFn func(T) K) *Collectio out = append(out, v) } - return &Collection[T]{items: out} + return Attach(out) } diff --git a/unique_comparable.go b/unique_comparable.go index fb126b0..0f67dea 100644 --- a/unique_comparable.go +++ b/unique_comparable.go @@ -33,7 +33,7 @@ package collection func UniqueComparable[T comparable](c *Collection[T]) *Collection[T] { n := len(c.items) if n == 0 { - return New([]T{}) + return Attach([]T{}) } seen := make(map[T]struct{}, n) @@ -48,5 +48,5 @@ func UniqueComparable[T comparable](c *Collection[T]) *Collection[T] { out = append(out, v) } - return &Collection[T]{items: out} + return Attach(out) } diff --git a/where.go b/where.go deleted file mode 100644 index 5a4e0ab..0000000 --- a/where.go +++ /dev/null @@ -1,52 +0,0 @@ -package collection - -// Where keeps only the elements for which fn returns true. -// This is an alias for Filter(fn) for SQL-style ergonomics. -// This method mutates the collection in place and returns the same instance. -// @group Slicing -// @behavior mutable -// @fluent true -// -// Example: integers -// -// nums := collection.New([]int{1, 2, 3, 4}) -// nums.Where(func(v int) bool { -// return v%2 == 0 -// }) -// collection.Dump(nums.Items()) -// // #[]int [ -// // 0 => 2 #int -// // 1 => 4 #int -// // ] -// -// 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"}, -// }) -// -// users.Where(func(u User) bool { -// return u.ID >= 2 -// }) -// -// collection.Dump(users.Items()) -// // #[]main.User [ -// // 0 => #main.User { -// // +ID => 2 #int -// // +Name => "Bob" #string -// // } -// // 1 => #main.User { -// // +ID => 3 #int -// // +Name => "Carol" #string -// // } -// // ] -func (c *Collection[T]) Where(fn func(T) bool) *Collection[T] { - return c.Filter(fn) -} diff --git a/where_test.go b/where_test.go deleted file mode 100644 index 1b10199..0000000 --- a/where_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package collection - -import ( - "reflect" - "testing" -) - -func TestWhere_AliasToFilter(t *testing.T) { - fn := func(v int) bool { return v%2 == 0 } - - c1 := New([]int{1, 2, 3, 4, 5}) - c2 := New([]int{1, 2, 3, 4, 5}) - - out1 := c1.Filter(fn) - out2 := c2.Where(fn) - - if out2 != c2 { - t.Fatalf("Where should return the same collection instance") - } - - if !reflect.DeepEqual(out1.items, out2.items) { - t.Fatalf("Where should behave exactly the same as Filter(fn)") - } -} diff --git a/window.go b/window.go index 86bfb8d..5ca84eb 100644 --- a/window.go +++ b/window.go @@ -7,6 +7,8 @@ package collection // @behavior allocates // @fluent true // +// NOTE: windows share the backing array with the source collection. +// // Example: integers - step 1 // // nums := collection.New([]int{1, 2, 3, 4, 5}) @@ -96,7 +98,7 @@ package collection // // ] func Window[T any](c *Collection[T], size int, step int) *Collection[[]T] { if size <= 0 { - return &Collection[[]T]{items: nil} + return Attach([][]T(nil)) } if step <= 0 { @@ -105,7 +107,7 @@ func Window[T any](c *Collection[T], size int, step int) *Collection[[]T] { n := len(c.items) if n < size { - return &Collection[[]T]{items: nil} + return Attach([][]T(nil)) } // Compute number of windows. @@ -113,10 +115,8 @@ func Window[T any](c *Collection[T], size int, step int) *Collection[[]T] { out := make([][]T, 0, count) for i := 0; i+size <= n; i += step { - window := make([]T, size) - copy(window, c.items[i:i+size]) - out = append(out, window) + out = append(out, c.items[i:i+size]) } - return &Collection[[]T]{items: out} + return Attach(out) } diff --git a/zip.go b/zip.go index d81253d..0ce416f 100644 --- a/zip.go +++ b/zip.go @@ -67,7 +67,7 @@ func Zip[A any, B any](a *Collection[A], b *Collection[B]) *Collection[Tuple[A, out[i] = Tuple[A, B]{First: a.items[i], Second: b.items[i]} } - return New(out) + return Attach(out) } // ZipWith combines two collections element-wise using combiner fn. @@ -138,5 +138,5 @@ func ZipWith[A any, B any, R any](a *Collection[A], b *Collection[B], fn func(A, out[i] = fn(a.items[i], b.items[i]) } - return New(out) + return Attach(out) } From 21b75d7ab4340e155c03367d9164286c983de101 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 20:22:17 -0600 Subject: [PATCH 02/30] docs: annotations --- README.md | 76 ++++++++++++++++++++++----------------------- all.go | 3 +- any.go | 3 +- at.go | 3 +- avg.go | 1 + chunk.go | 3 +- collection.go | 6 ++-- contains.go | 3 +- count.go | 3 +- count_by.go | 2 ++ docs/readme/main.go | 19 +++++++++++- dump.go | 7 +++-- first.go | 3 +- first_where.go | 3 +- group_by.go | 1 + group_by_slice.go | 1 + index_where.go | 3 +- is_empty.go | 3 +- last.go | 3 +- last_where.go | 3 +- max.go | 1 + max_by.go | 1 + median.go | 1 + min.go | 1 + min_by.go | 1 + mode.go | 1 + none.go | 3 +- pipe.go | 3 +- pop.go | 2 ++ reduce.go | 3 +- sum.go | 1 + to_json.go | 6 ++-- to_map.go | 1 + to_map_kv.go | 1 + transform.go | 3 +- 35 files changed, 117 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 836fa78..2952be8 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ go get github.com/goforj/collection ## Access -### Items · readonly · fluent +### Items · readonly · terminal Items returns the backing slice of items. @@ -331,7 +331,7 @@ collection.Dump(out) // ] ``` -### ItemsCopy · readonly · fluent +### ItemsCopy · readonly · terminal ItemsCopy returns a copy of the collection's items. @@ -348,7 +348,7 @@ collection.Dump(items) ## Aggregation -### Avg · readonly +### Avg · readonly · terminal Avg returns the average of the collection values as a float64. If the collection is empty, Avg returns 0. @@ -369,7 +369,7 @@ collection.Dump(c2.Avg()) // 2.333333 #float64 ``` -### Count · readonly · fluent +### Count · readonly · terminal Count returns the total number of items in the collection. @@ -379,7 +379,7 @@ collection.Dump(count) // 4 #int ``` -### CountBy · readonly +### CountBy · readonly · terminal CountBy returns a map of keys extracted by fn to their occurrence counts. K must be comparable. @@ -441,7 +441,7 @@ collection.Dump(roleCounts) // } ``` -### CountByValue · readonly +### CountByValue · readonly · terminal CountByValue returns a map where each distinct item in the collection is mapped to the number of times it appears. @@ -493,7 +493,7 @@ collection.Dump(counts3) // ] ``` -### Max · readonly +### Max · readonly · terminal Max returns the largest numeric item in the collection. The second return value is false if the collection is empty. @@ -531,7 +531,7 @@ collection.Dump(max3, ok3) // false #bool ``` -### MaxBy · readonly +### 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. @@ -586,7 +586,7 @@ collection.Dump(maxVal, ok) // false #bool ``` -### Median · readonly +### Median · readonly · terminal Median returns the statistical median of the numeric collection as float64. Returns (0, false) if the collection is empty. @@ -635,7 +635,7 @@ collection.Dump(median4, ok4) // false #bool ``` -### Min · readonly +### Min · readonly · terminal Min returns the smallest numeric item in the collection. The second return value is false if the collection is empty. @@ -670,7 +670,7 @@ collection.Dump(min3, ok3) // false #bool ``` -### MinBy · readonly +### 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. @@ -725,7 +725,7 @@ collection.Dump(minVal, ok) // false #bool ``` -### Mode · readonly +### Mode · readonly · terminal Mode returns the most frequent numeric value(s) in the collection. If multiple values tie for highest frequency, all are returned @@ -774,7 +774,7 @@ collection.Dump(mode4) // ``` -### Reduce · readonly · fluent +### Reduce · readonly · terminal Reduce collapses the collection into a single accumulated value. The accumulator has the same type T as the collection's elements. @@ -826,7 +826,7 @@ collection.Dump(total) // ] ``` -### Sum · readonly +### 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. @@ -968,7 +968,7 @@ NewNumeric wraps a slice of numeric types in a NumericCollection. ## Debugging -### Dd · fluent +### Dd · terminal Dd prints items then terminates execution. Like Laravel's dd(), this is intended for debugging and @@ -1025,7 +1025,7 @@ collection.Dump(c2.Items()) // ] ``` -### DumpStr · readonly · fluent +### DumpStr · readonly · terminal DumpStr returns the pretty-printed dump of the items as a string, without printing or exiting. @@ -1043,7 +1043,7 @@ fmt.Println(s) ## Grouping -### GroupBy · readonly +### GroupBy · readonly · terminal GroupBy partitions the collection into groups keyed by the value returned from keyFn. @@ -1115,7 +1115,7 @@ collection.Dump(groups2["user"].Items()) // ] ``` -### GroupBySlice · readonly +### GroupBySlice · readonly · terminal GroupBySlice partitions the collection into groups keyed by the value returned from keyFn. @@ -1260,7 +1260,7 @@ collection.Dump(out2) // ] ``` -### ToMap · readonly +### ToMap · readonly · terminal ToMap reduces a collection into a map using the provided key and value selector functions. @@ -1301,7 +1301,7 @@ byID := collection.ToMap( collection.Dump(byID) ``` -### ToMapKV · readonly +### ToMapKV · readonly · terminal ToMapKV converts a collection of key/value pairs into a map. @@ -1593,7 +1593,7 @@ collection.Dump(users.Items()) ## Querying -### All · readonly · fluent +### 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). @@ -1634,7 +1634,7 @@ collection.Dump(all) // true #bool ``` -### Any · readonly · fluent +### Any · readonly · terminal Any returns true if at least one item satisfies fn. @@ -1645,7 +1645,7 @@ collection.Dump(has) // true #bool ``` -### At · readonly · fluent +### At · readonly · terminal At returns the item at the given index and a boolean indicating whether the index was within bounds. @@ -1685,7 +1685,7 @@ collection.Dump(u, ok3) // {ID:1 Name:"Alice"} true ``` -### Contains · readonly · fluent +### Contains · readonly · terminal Contains returns true if the collection contains the given value. @@ -1707,7 +1707,7 @@ collection.Dump(hasBanana) // true #bool ``` -### First · readonly · fluent +### First · readonly · terminal First returns the first element in the collection. If the collection is empty, ok will be false. @@ -1766,7 +1766,7 @@ collection.Dump(v3, ok4) // false #bool ``` -### FirstWhere · readonly · fluent +### 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 @@ -1789,7 +1789,7 @@ collection.Dump(v, ok) // false #bool ``` -### IndexWhere · readonly · fluent +### IndexWhere · readonly · terminal IndexWhere returns the index of the first item in the collection for which the provided predicate function returns true. @@ -1834,7 +1834,7 @@ collection.Dump(idx3, ok3) // 1 true ``` -### IsEmpty · readonly · fluent +### IsEmpty · readonly · terminal IsEmpty returns true if the collection has no items. @@ -1885,7 +1885,7 @@ collection.Dump(empty4) // true #bool ``` -### Last · readonly · fluent +### Last · readonly · terminal Last returns the last element in the collection. If the collection is empty, ok will be false. @@ -1946,7 +1946,7 @@ collection.Dump(v3, ok4) // false #bool ``` -### LastWhere · readonly · fluent +### 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. @@ -2039,7 +2039,7 @@ collection.Dump(v5, ok6) // false #bool ``` -### None · readonly · fluent +### None · readonly · terminal None returns true if fn returns false for every item in the collection. If the collection is empty, None returns true. @@ -2073,7 +2073,7 @@ collection.Dump(none) ## Serialization -### ToJSON · readonly · fluent +### ToJSON · readonly · terminal ToJSON converts the collection's items into a compact JSON string. @@ -2084,7 +2084,7 @@ fmt.Println(out1) // ["a","b"] ``` -### ToPrettyJSON · readonly · fluent +### ToPrettyJSON · readonly · terminal ToPrettyJSON converts the collection's items into a human-readable, indented JSON string. @@ -2531,7 +2531,7 @@ collection.Dump(out2.Items()) ## Slicing -### Chunk · readonly · fluent +### 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. @@ -2745,7 +2745,7 @@ collection.Dump(active.Items(), inactive.Items()) // ] ``` -### Pop · mutable +### Pop · mutable · terminal Pop removes and returns the last item in the collection. @@ -2817,7 +2817,7 @@ collection.Dump(item4, ok4, empty.Items()) // ] ``` -### PopN · mutable +### PopN · mutable · terminal PopN removes and returns the last n items in original order. @@ -3754,7 +3754,7 @@ collection.Dump(none.Items()) // ] ``` -### Pipe · readonly · fluent +### Pipe · readonly · terminal Pipe passes the entire collection into the given function and returns the function's result. @@ -4026,7 +4026,7 @@ collection.Dump(cTimes3.Items()) // ] ``` -### Transform · mutable · fluent +### Transform · mutable · terminal Transform applies fn to every item *in place*, mutating the collection. diff --git a/all.go b/all.go index ca1a398..9801511 100644 --- a/all.go +++ b/all.go @@ -4,7 +4,8 @@ package collection // If the collection is empty, All returns true (vacuously true). // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // Example: integers – all even // diff --git a/any.go b/any.go index ef93ab3..1586ca7 100644 --- a/any.go +++ b/any.go @@ -3,7 +3,8 @@ package collection // Any returns true if at least one item satisfies fn. // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // Example: integers // // c := collection.New([]int{1, 2, 3, 4}) diff --git a/at.go b/at.go index 5f07d4a..9d969cf 100644 --- a/at.go +++ b/at.go @@ -4,7 +4,8 @@ package collection // whether the index was within bounds. // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // This method is safe and does not panic for out-of-range indices. // diff --git a/avg.go b/avg.go index b402d94..67f7697 100644 --- a/avg.go +++ b/avg.go @@ -5,6 +5,7 @@ package collection // @group Aggregation // @behavior readonly // @fluent false +// @terminal true // // Example: integers // diff --git a/chunk.go b/chunk.go index 817aeb0..d158529 100644 --- a/chunk.go +++ b/chunk.go @@ -4,7 +4,8 @@ package collection // The final chunk may be smaller if len(items) is not divisible by size. // @group Slicing // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // If size <= 0, nil is returned. // diff --git a/collection.go b/collection.go index fa3a5a2..f7224c5 100644 --- a/collection.go +++ b/collection.go @@ -117,7 +117,8 @@ func AttachNumeric[T Number](items []T) *NumericCollection[T] { // Items returns the backing slice of items. // @group Access // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // Items shares the backing array with the collection. Mutating the returned // slice will mutate the collection. @@ -174,7 +175,8 @@ func (c *Collection[T]) Items() []T { // ItemsCopy returns a copy of the collection's items. // @group Access // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // ItemsCopy allocates a new slice. // diff --git a/contains.go b/contains.go index 79ce741..d800d8f 100644 --- a/contains.go +++ b/contains.go @@ -3,7 +3,8 @@ package collection // Contains returns true if the collection contains the given value. // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // Similar to: Any (differs by using equality on a value). // diff --git a/count.go b/count.go index 164476b..e840be7 100644 --- a/count.go +++ b/count.go @@ -3,7 +3,8 @@ package collection // Count returns the total number of items in the collection. // @group Aggregation // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // Example: integers // // count := collection.New([]int{1, 2, 3, 4}).Count() diff --git a/count_by.go b/count_by.go index 2d2c966..adc9b2e 100644 --- a/count_by.go +++ b/count_by.go @@ -5,6 +5,7 @@ package collection // @group Aggregation // @behavior readonly // @fluent false +// @terminal true // // Example: integers // @@ -70,6 +71,7 @@ func CountBy[T any, K comparable](c *Collection[T], fn func(T) K) map[K]int { // @group Aggregation // @behavior readonly // @fluent false +// @terminal true // // T must be comparable. // diff --git a/docs/readme/main.go b/docs/readme/main.go index b4d4948..1c7c63f 100644 --- a/docs/readme/main.go +++ b/docs/readme/main.go @@ -81,6 +81,7 @@ type FuncDoc struct { Group string Behavior string Fluent string + Terminal string Description string Examples []Example } @@ -101,6 +102,7 @@ var ( groupHeader = regexp.MustCompile(`(?i)^\s*@group\s+(.+)$`) behaviorHeader = regexp.MustCompile(`(?i)^\s*@behavior\s+(.+)$`) fluentHeader = regexp.MustCompile(`(?i)^\s*@fluent\s+(.+)$`) + terminalHeader = regexp.MustCompile(`(?i)^\s*@terminal\s+(.+)$`) exampleHeader = regexp.MustCompile(`(?i)^\s*Example:\s*(.*)$`) ) @@ -147,6 +149,7 @@ func parseFuncs(root string) ([]*FuncDoc, error) { Group: extractGroup(fn.Doc), Behavior: extractBehavior(fn.Doc), Fluent: extractFluent(fn.Doc), + Terminal: extractTerminal(fn.Doc), Description: extractDescription(fn.Doc), Examples: extractExamples(fset, fn), } @@ -200,6 +203,16 @@ func extractFluent(group *ast.CommentGroup) string { return "" } +func extractTerminal(group *ast.CommentGroup) string { + for _, c := range group.List { + line := strings.TrimSpace(strings.TrimPrefix(c.Text, "//")) + if m := terminalHeader.FindStringSubmatch(line); m != nil { + return strings.ToLower(strings.TrimSpace(m[1])) + } + } + return "" +} + func extractDescription(group *ast.CommentGroup) string { var lines []string @@ -209,7 +222,8 @@ func extractDescription(group *ast.CommentGroup) string { if exampleHeader.MatchString(line) || groupHeader.MatchString(line) || behaviorHeader.MatchString(line) || - fluentHeader.MatchString(line) { + fluentHeader.MatchString(line) || + terminalHeader.MatchString(line) { break } @@ -372,6 +386,9 @@ func renderAPI(funcs []*FuncDoc) string { if fn.Fluent == "true" { header += " · fluent" } + if fn.Terminal == "true" { + header += " · terminal" + } buf.WriteString(fmt.Sprintf("### %s\n\n", anchor, header)) diff --git a/dump.go b/dump.go index 5915672..d258a4f 100644 --- a/dump.go +++ b/dump.go @@ -43,7 +43,8 @@ func (c *Collection[T]) Dump() *Collection[T] { // Like Laravel's dd(), this is intended for debugging and // should not be used in production control flow. // @group Debugging -// @fluent true +// @fluent false +// @terminal true // // This method never returns. // @@ -65,7 +66,8 @@ func (c *Collection[T]) Dd() { // Useful for logging, snapshot testing, and non-interactive debugging. // @group Debugging // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // Example: integers // @@ -91,6 +93,7 @@ func setDumpWriter(w io.Writer) { // Dump is a convenience function that calls godump.Dump. // @group Debugging // @fluent false +// @terminal true // // Example: integers // diff --git a/first.go b/first.go index 2ddf27e..4701e1d 100644 --- a/first.go +++ b/first.go @@ -4,7 +4,8 @@ package collection // If the collection is empty, ok will be false. // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // Example: integers // diff --git a/first_where.go b/first_where.go index 2c98e42..a1a8493 100644 --- a/first_where.go +++ b/first_where.go @@ -5,7 +5,8 @@ package collection // along with the zero value of T. // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // This method is equivalent to Laravel's collection->first(fn) and mirrors // the behavior found in functional collections in other languages. diff --git a/group_by.go b/group_by.go index 96bf2aa..3aeee84 100644 --- a/group_by.go +++ b/group_by.go @@ -5,6 +5,7 @@ package collection // @group Grouping // @behavior readonly // @fluent false +// @terminal true // // The order of items within each group is preserved. // The order of the groups themselves is unspecified. diff --git a/group_by_slice.go b/group_by_slice.go index 092d44e..5166733 100644 --- a/group_by_slice.go +++ b/group_by_slice.go @@ -5,6 +5,7 @@ package collection // @group Grouping // @behavior readonly // @fluent false +// @terminal true // // The order of items within each group is preserved. // The order of the groups themselves is unspecified. diff --git a/index_where.go b/index_where.go index 02be1c7..801ab2a 100644 --- a/index_where.go +++ b/index_where.go @@ -5,7 +5,8 @@ package collection // If no item matches, it returns (0, false). // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // This operation performs no allocations and short-circuits // on the first match. diff --git a/is_empty.go b/is_empty.go index f060e42..0fda8a0 100644 --- a/is_empty.go +++ b/is_empty.go @@ -3,7 +3,8 @@ package collection // IsEmpty returns true if the collection has no items. // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // Example: integers (non-empty) // diff --git a/last.go b/last.go index 2cf5665..5d19f33 100644 --- a/last.go +++ b/last.go @@ -4,7 +4,8 @@ package collection // If the collection is empty, ok will be false. // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // Example: integers // diff --git a/last_where.go b/last_where.go index baef909..973b5ca 100644 --- a/last_where.go +++ b/last_where.go @@ -5,7 +5,8 @@ package collection // If the collection is empty or no element matches, ok will be false. // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // Example: integers // diff --git a/max.go b/max.go index 1a3ba78..dc2d9f0 100644 --- a/max.go +++ b/max.go @@ -5,6 +5,7 @@ package collection // @group Aggregation // @behavior readonly // @fluent false +// @terminal true // // Example: integers // diff --git a/max_by.go b/max_by.go index ee78d92..4c17202 100644 --- a/max_by.go +++ b/max_by.go @@ -5,6 +5,7 @@ package collection // @group Aggregation // @behavior readonly // @fluent false +// @terminal true // // This cannot be a method because methods can't introduce a new type parameter K. // When multiple items share the same maximal key, the first such item is returned. diff --git a/median.go b/median.go index abde321..9ffbaf6 100644 --- a/median.go +++ b/median.go @@ -7,6 +7,7 @@ import "sort" // @group Aggregation // @behavior readonly // @fluent false +// @terminal true // // Odd count → middle value // Even count → average of the two middle values diff --git a/min.go b/min.go index 09360a5..1122007 100644 --- a/min.go +++ b/min.go @@ -5,6 +5,7 @@ package collection // @group Aggregation // @behavior readonly // @fluent false +// @terminal true // // Example: integers // diff --git a/min_by.go b/min_by.go index f3b69e2..ae033ed 100644 --- a/min_by.go +++ b/min_by.go @@ -5,6 +5,7 @@ package collection // @group Aggregation // @behavior readonly // @fluent false +// @terminal true // // This cannot be a method because methods can't introduce a new type parameter K. // When multiple items share the same minimal key, the first such item is returned. diff --git a/mode.go b/mode.go index 2bc382b..f23d4b8 100644 --- a/mode.go +++ b/mode.go @@ -6,6 +6,7 @@ package collection // @group Aggregation // @behavior readonly // @fluent false +// @terminal true // // Example: integers – single mode // diff --git a/none.go b/none.go index f89dbc1..cca2793 100644 --- a/none.go +++ b/none.go @@ -4,7 +4,8 @@ package collection // If the collection is empty, None returns true. // @group Querying // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // Example: integers – none even // diff --git a/pipe.go b/pipe.go index 6d87010..023b6e6 100644 --- a/pipe.go +++ b/pipe.go @@ -4,7 +4,8 @@ package collection // and returns the function's result. // @group Transformation // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // This is a typed escape hatch for branching into arbitrary logic without `any`. // diff --git a/pop.go b/pop.go index 3eeed93..679a482 100644 --- a/pop.go +++ b/pop.go @@ -4,6 +4,7 @@ package collection // @group Slicing // @behavior mutable // @fluent false +// @terminal true // // If the collection is empty, the zero value of T is returned with ok=false. // @@ -83,6 +84,7 @@ func (c *Collection[T]) Pop() (T, bool) { // @group Slicing // @behavior mutable // @fluent false +// @terminal true // // Example: integers – pop 2 // diff --git a/reduce.go b/reduce.go index 51ef0a1..4a26185 100644 --- a/reduce.go +++ b/reduce.go @@ -4,7 +4,8 @@ package collection // The accumulator has the same type T as the collection's elements. // @group Aggregation // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // This is useful for computing sums, concatenations, aggregates, // or any fold-style reduction. diff --git a/sum.go b/sum.go index ac8b70b..1580281 100644 --- a/sum.go +++ b/sum.go @@ -5,6 +5,7 @@ package collection // @group Aggregation // @behavior readonly // @fluent false +// @terminal true // // Example: integers // diff --git a/to_json.go b/to_json.go index bc899bb..6ae5c7d 100644 --- a/to_json.go +++ b/to_json.go @@ -8,7 +8,8 @@ import ( // ToJSON converts the collection's items into a compact JSON string. // @group Serialization // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // If marshalling succeeds, a JSON-encoded string and a nil error are returned. // If marshalling fails, the method unwraps any json.Marshal wrapping so that @@ -36,7 +37,8 @@ func (c *Collection[T]) ToJSON() (string, error) { // indented JSON string. // @group Serialization // @behavior readonly -// @fluent true +// @fluent false +// @terminal true // // If marshalling succeeds, a formatted JSON string and nil error are returned. // If marshalling fails, the underlying error is unwrapped so user-defined diff --git a/to_map.go b/to_map.go index cfdde11..947612d 100644 --- a/to_map.go +++ b/to_map.go @@ -5,6 +5,7 @@ package collection // @group Maps // @behavior readonly // @fluent false +// @terminal true // // If multiple items produce the same key, the last value wins. // diff --git a/to_map_kv.go b/to_map_kv.go index 23c138a..772f4d7 100644 --- a/to_map_kv.go +++ b/to_map_kv.go @@ -4,6 +4,7 @@ package collection // @group Maps // @behavior readonly // @fluent false +// @terminal true // // If multiple pairs contain the same key, the last value wins. // diff --git a/transform.go b/transform.go index 6a1b788..a5a937e 100644 --- a/transform.go +++ b/transform.go @@ -3,7 +3,8 @@ package collection // Transform applies fn to every item *in place*, mutating the collection. // @group Transformation // @behavior mutable -// @fluent true +// @fluent false +// @terminal true // // This mirrors Laravel's transform(), which modifies the underlying values // instead of returning a new collection. From 492d52bc99a2bc4d805f636d922d31c09e3ffc76 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 20:31:04 -0600 Subject: [PATCH 03/30] docs: update benchmarks --- README.md | 93 ++++++++++++++++++++++------------------------ docs/bench/main.go | 2 +- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 2952be8..0c6bcc4 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ - **Fluent chaining** - pipeline your operations like Laravel Collections - **Fully generic** (`Collection[T]`) - no reflection, no `interface{}` -- **Zero dependencies** - pure Go, fast, lightweight -- **Minimal allocations** - avoids unnecessary copies; most operations reuse the underlying slice +- **Minimal dependencies** - small footprint (godump for debugging) +- **Minimal allocations** - views for slice selections; in-place ops reuse backing when possible - **Map / Filter / Reduce** - clean functional transforms -- **First / Last / Find / Contains** helpers +- **First / Last / FirstWhere / IndexWhere / Contains** helpers - **Sort, GroupBy, Chunk**, and more - **Safe-by-default** - defensive copies where appropriate - **Built-in JSON helpers** (`ToJSON()`, `ToPrettyJSON()`) @@ -79,43 +79,51 @@ collection. ### Performance Benchmarks -[lo](https://github.com/samber/lo) is a fantastic library and a major inspiration for this project. Our focus differs: `collection` is built for fluent chaining with explicit mutability, which lets hot paths avoid intermediate allocations. That shows up most in chained pipelines and in-place operations where we can keep work on the same backing slice while still being explicit about behavior. +> **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. | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 232ns / 230ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Any** | 232ns / 234ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Chunk** | 128ns / 1.1µs | **8.30x** | 1.3KB / 9.3KB | **7.25x less** | 1 / 51 | -| **Contains** | 238ns / 250ns | **1.05x** | 0B / 0B | ≈ | 0 / 0 | -| **CountBy** | 8.1µs / 8.2µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 | -| **CountByValue** | 8.1µs / 8.1µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 | -| **Difference** | 19.4µs / 44.5µs | **2.29x** | 82.1KB / 108.8KB | **1.33x less** | 12 / 43 | -| **Each** | 235ns / 230ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Filter** | 647ns / 1.1µs | **1.67x** | 0B / 8.2KB | **∞x less** | 0 / 1 | -| **Find** | 239ns / 235ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **First** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | -| **GroupBySlice** | 8.2µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 83 / 83 | -| **IndexWhere** | 232ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Intersect** | 11.0µs / 10.8µs | ≈ | 11.4KB / 11.4KB | ≈ | 19 / 19 | -| **Last** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | -| **Map** | 347ns / 821ns | **2.37x** | 0B / 8.2KB | **∞x less** | 0 / 1 | -| **Max** | 230ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Min** | 232ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **None** | 232ns / 232ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Pipeline F→M→T→R** | 496ns / 1.3µs | **2.62x** | 0B / 12.3KB | **∞x less** | 0 / 2 | -| **Reduce (sum)** | 230ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Reverse** | 216ns / 230ns | **1.06x** | 0B / 0B | ≈ | 0 / 0 | -| **Shuffle** | 3.6µs / 5.3µs | **1.49x** | 0B / 0B | ≈ | 0 / 0 | -| **Skip** | 0ns / 721ns | ∞ | 0B / 8.2KB | **∞x less** | 0 / 1 | -| **SkipLast** | 0ns / 730ns | ∞ | 0B / 8.2KB | **∞x less** | 0 / 1 | -| **Sum** | 232ns / 233ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Take** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | -| **ToMap** | 7.7µs / 7.8µs | ≈ | 36.9KB / 37.0KB | ≈ | 5 / 6 | -| **Union** | 17.4µs / 17.7µs | ≈ | 90.3KB / 90.3KB | ≈ | 11 / 10 | -| **Unique** | 6.4µs / 6.5µs | ≈ | 45.1KB / 45.1KB | ≈ | 6 / 6 | -| **UniqueBy** | 6.9µs / 6.7µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **Zip** | 1.4µs / 3.2µs | **2.27x** | 16.4KB / 16.4KB | ≈ | 1 / 1 | -| **ZipWith** | 1.0µs / 3.1µs | **3.07x** | 8.2KB / 8.2KB | ≈ | 1 / 1 | +| **All** | 945ns / 278ns | 0.29x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **Any** | 812ns / 281ns | 0.35x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **Chunk** | 772ns / 1.0µs | **1.35x** | 9.5KB / 9.3KB | ≈ | 2 / 51 | +| **Contains** | 777ns / 237ns | 0.31x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.7µs / 8.5µs | ≈ | 17.5KB / 9.4KB | 0.53x more | 12 / 11 | +| **CountByValue** | 8.6µs / 8.4µs | ≈ | 17.5KB / 9.4KB | 0.53x more | 12 / 11 | +| **Difference** | 20.1µs / 43.3µs | **2.16x** | 98.5KB / 108.8KB | **1.10x less** | 14 / 43 | +| **Each** | 898ns / 281ns | 0.31x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **Filter** | 1.1µs / 1.1µs | ≈ | 8.2KB / 8.2KB | ≈ | 1 / 1 | +| **First** | 653ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **FirstWhere** | 793ns / 238ns | 0.30x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 8.7µs / 8.4µs | ≈ | 29.2KB / 21.0KB | 0.72x more | 84 / 83 | +| **IndexWhere** | 821ns / 287ns | 0.35x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.8µs / 10.7µs | 0.90x | 27.8KB / 11.4KB | 0.41x more | 22 / 19 | +| **Last** | 656ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **Map** | 1.5µs / 807ns | 0.52x | 16.4KB / 8.2KB | 0.50x more | 2 / 1 | +| **Max** | 780ns / 229ns | 0.29x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **Min** | 792ns / 229ns | 0.29x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **None** | 800ns / 254ns | 0.32x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 1.4µs / 1.4µs | ≈ | 12.3KB / 12.3KB | ≈ | 3 / 2 | +| **Reduce (sum)** | 773ns / 231ns | 0.30x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **Reverse** | 785ns / 232ns | 0.30x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 4.4µs / 5.3µs | **1.21x** | 16.4KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 657ns / 696ns | **1.06x** | 8.2KB / 8.2KB | ≈ | 1 / 1 | +| **SkipLast** | 657ns / 703ns | **1.07x** | 8.2KB / 8.2KB | ≈ | 1 / 1 | +| **Sum** | 777ns / 229ns | 0.29x | 8.2KB / 0B | ∞x more | 1 / 0 | +| **Take** | 677ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **ToMap** | 8.2µs / 7.9µs | ≈ | 45.1KB / 37.0KB | 0.82x more | 6 / 6 | +| **Union** | 18.1µs / 17.7µs | ≈ | 106.7KB / 90.3KB | 0.85x more | 13 / 10 | +| **Unique** | 6.9µs / 6.3µs | 0.90x | 53.3KB / 45.1KB | 0.85x more | 7 / 6 | +| **UniqueBy** | 7.2µs / 6.3µs | 0.88x | 53.4KB / 45.1KB | 0.85x more | 8 / 6 | +| **Zip** | 2.6µs / 3.2µs | **1.24x** | 32.8KB / 16.4KB | 0.50x more | 3 / 1 | +| **ZipWith** | 2.2µs / 3.1µs | **1.38x** | 24.6KB / 8.2KB | 0.33x more | 3 / 1 | ## How to read the benchmarks @@ -128,17 +136,6 @@ collection. If you prefer immutable, one-off helpers - `lo` is outstanding. If you write **expressive, chained data pipelines** and care about hot-path performance - `collection` is built for that job. -## Performance Philosophy - -> **tl;dr**: *lo* is excellent. We solve a different problem - and in chained pipelines, that difference matters. - -`lo` is a fantastic library and a major inspiration for this project. It is battle-tested, idiomatic, and often the right choice when you want small, standalone helpers that operate on slices in isolation. - -`collection` takes a different approach. - -Rather than treating each operation as an independent transformation, `collection` is built around **explicit, fluent pipelines**. Many operations are designed to **mutate the same backing slice intentionally**, allowing chained workflows to avoid intermediate allocations and unnecessary copying - while still making that behavior visible and documented. - -That design choice doesn't matter much for some single operations. It matters a *lot* once you start chaining and especially in hot paths. ## Why chaining changes the performance story diff --git a/docs/bench/main.go b/docs/bench/main.go index 2bfffeb..97751a3 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -73,7 +73,7 @@ func runBenches(only map[string]struct{}) []benchResult { {"Chunk", benchChunkCollection, benchChunkLo}, {"Take", benchTakeCollection, benchTakeLo}, {"Contains", benchContainsCollection, benchContainsLo}, - {"Find", benchFindCollection, benchFindLo}, + {"FirstWhere", benchFindCollection, benchFindLo}, {"GroupBySlice", benchGroupByCollection, benchGroupByLo}, {"CountBy", benchCountByCollection, benchCountByLo}, {"CountByValue", benchCountByValueCollection, benchCountByValueLo}, From 24a4f2029ef7f8a3330068b5745adf3073b1a17d Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 20:44:30 -0600 Subject: [PATCH 04/30] docs: benchmarks --- README.md | 70 +++++++++++++++-------------- docs/bench/main.go | 110 ++++++++++++++++++++++++++++++--------------- 2 files changed, 109 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 0c6bcc4..44a516f 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ collection. // ] ``` - + ### Performance Benchmarks @@ -89,41 +89,43 @@ Rather than treating each operation as an independent transformation, `collectio That design choice doesn't matter much for some single operations. It matters a *lot* once you start chaining and especially in hot paths. + + | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 945ns / 278ns | 0.29x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **Any** | 812ns / 281ns | 0.35x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **Chunk** | 772ns / 1.0µs | **1.35x** | 9.5KB / 9.3KB | ≈ | 2 / 51 | -| **Contains** | 777ns / 237ns | 0.31x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.7µs / 8.5µs | ≈ | 17.5KB / 9.4KB | 0.53x more | 12 / 11 | -| **CountByValue** | 8.6µs / 8.4µs | ≈ | 17.5KB / 9.4KB | 0.53x more | 12 / 11 | -| **Difference** | 20.1µs / 43.3µs | **2.16x** | 98.5KB / 108.8KB | **1.10x less** | 14 / 43 | -| **Each** | 898ns / 281ns | 0.31x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **Filter** | 1.1µs / 1.1µs | ≈ | 8.2KB / 8.2KB | ≈ | 1 / 1 | -| **First** | 653ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 793ns / 238ns | 0.30x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 8.7µs / 8.4µs | ≈ | 29.2KB / 21.0KB | 0.72x more | 84 / 83 | -| **IndexWhere** | 821ns / 287ns | 0.35x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **Intersect** | 11.8µs / 10.7µs | 0.90x | 27.8KB / 11.4KB | 0.41x more | 22 / 19 | -| **Last** | 656ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **Map** | 1.5µs / 807ns | 0.52x | 16.4KB / 8.2KB | 0.50x more | 2 / 1 | -| **Max** | 780ns / 229ns | 0.29x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **Min** | 792ns / 229ns | 0.29x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **None** | 800ns / 254ns | 0.32x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 1.4µs / 1.4µs | ≈ | 12.3KB / 12.3KB | ≈ | 3 / 2 | -| **Reduce (sum)** | 773ns / 231ns | 0.30x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **Reverse** | 785ns / 232ns | 0.30x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 4.4µs / 5.3µs | **1.21x** | 16.4KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 657ns / 696ns | **1.06x** | 8.2KB / 8.2KB | ≈ | 1 / 1 | -| **SkipLast** | 657ns / 703ns | **1.07x** | 8.2KB / 8.2KB | ≈ | 1 / 1 | -| **Sum** | 777ns / 229ns | 0.29x | 8.2KB / 0B | ∞x more | 1 / 0 | -| **Take** | 677ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **ToMap** | 8.2µs / 7.9µs | ≈ | 45.1KB / 37.0KB | 0.82x more | 6 / 6 | -| **Union** | 18.1µs / 17.7µs | ≈ | 106.7KB / 90.3KB | 0.85x more | 13 / 10 | -| **Unique** | 6.9µs / 6.3µs | 0.90x | 53.3KB / 45.1KB | 0.85x more | 7 / 6 | -| **UniqueBy** | 7.2µs / 6.3µs | 0.88x | 53.4KB / 45.1KB | 0.85x more | 8 / 6 | -| **Zip** | 2.6µs / 3.2µs | **1.24x** | 32.8KB / 16.4KB | 0.50x more | 3 / 1 | -| **ZipWith** | 2.2µs / 3.1µs | **1.38x** | 24.6KB / 8.2KB | 0.33x more | 3 / 1 | +| **All** | 234ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Any** | 232ns / 233ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Chunk** | 125ns / 1.0µs | **8.31x** | 1.3KB / 9.3KB | **7.25x less** | 1 / 51 | +| **Contains** | 242ns / 242ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **CountBy** | 8.6µs / 8.4µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 | +| **CountByValue** | 8.5µs / 8.7µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 | +| **Difference** | 19.1µs / 44.2µs | **2.32x** | 82.1KB / 108.8KB | **1.33x less** | 12 / 43 | +| **Each** | 237ns / 249ns | **1.05x** | 0B / 0B | ≈ | 0 / 0 | +| **Filter** | 636ns / 1.1µs | **1.78x** | 0B / 8.2KB | **∞x less** | 0 / 1 | +| **First** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | +| **FirstWhere** | 248ns / 236ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **GroupBySlice** | 8.1µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 83 / 83 | +| **IndexWhere** | 235ns / 238ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Intersect** | 11.1µs / 10.8µs | ≈ | 11.4KB / 11.4KB | ≈ | 20 / 19 | +| **Last** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | +| **Map** | 826ns / 806ns | ≈ | 8.2KB / 8.2KB | ≈ | 1 / 1 | +| **Max** | 229ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Min** | 229ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **None** | 234ns / 240ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Pipeline F→M→T→R** | 917ns / 1.3µs | **1.38x** | 4.1KB / 12.3KB | **2.98x less** | 2 / 2 | +| **Reduce (sum)** | 232ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Reverse** | 221ns / 236ns | **1.07x** | 0B / 0B | ≈ | 0 / 0 | +| **Shuffle** | 4.1µs / 5.4µs | **1.33x** | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Skip** | 0ns / 692ns | ∞ | 0B / 8.2KB | **∞x less** | 0 / 1 | +| **SkipLast** | 0ns / 693ns | ∞ | 0B / 8.2KB | **∞x less** | 0 / 1 | +| **Sum** | 231ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Take** | 11ns / 0ns | 0.00x | 24B / 0B | ∞x more | 1 / 0 | +| **ToMap** | 7.7µs / 7.9µs | ≈ | 36.9KB / 37.0KB | ≈ | 5 / 6 | +| **Union** | 17.2µs / 17.7µs | ≈ | 90.3KB / 90.3KB | ≈ | 11 / 10 | +| **Unique** | 6.6µs / 6.6µs | ≈ | 45.1KB / 45.1KB | ≈ | 6 / 6 | +| **UniqueBy** | 7.1µs / 6.8µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **Zip** | 1.4µs / 3.4µs | **2.40x** | 16.4KB / 16.4KB | ≈ | 1 / 1 | +| **ZipWith** | 1.0µs / 3.1µs | **3.01x** | 8.2KB / 8.2KB | ≈ | 1 / 1 | ## How to read the benchmarks diff --git a/docs/bench/main.go b/docs/bench/main.go index 97751a3..d3b255a 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -192,7 +192,7 @@ func benchPipelineCollection(b *testing.B) { for i := 0; i < b.N; i++ { copy(workA, benchInts) - _ = collection.New(workA). + _ = collection.Attach(workA). Filter(func(v int) bool { return v%2 == 0 }). Map(func(v int) int { return v * v }). Take(benchPipelineLen). @@ -215,7 +215,7 @@ func benchPipelineLo(b *testing.B) { func benchAllCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.New(benchInts).All(func(v int) bool { return v < benchSize+1 }) + _ = collection.Attach(benchInts).All(func(v int) bool { return v < benchSize+1 }) } } @@ -229,7 +229,7 @@ func benchAllLo(b *testing.B) { func benchAnyCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.New(benchInts).Any(func(v int) bool { return v == benchSize-1 }) + _ = collection.Attach(benchInts).Any(func(v int) bool { return v == benchSize-1 }) } } @@ -243,7 +243,7 @@ func benchAnyLo(b *testing.B) { func benchNoneCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.New(benchInts).None(func(v int) bool { return v < 0 }) + _ = collection.Attach(benchInts).None(func(v int) bool { return v < 0 }) } } @@ -257,7 +257,7 @@ func benchNoneLo(b *testing.B) { func benchFirstCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.New(benchInts).First() + _, _ = collection.Attach(benchInts).First() } } @@ -271,7 +271,7 @@ func benchFirstLo(b *testing.B) { func benchLastCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.New(benchInts).Last() + _, _ = collection.Attach(benchInts).Last() } } @@ -285,7 +285,7 @@ func benchLastLo(b *testing.B) { func benchIndexWhereCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.New(benchInts).IndexWhere(func(v int) bool { return v == benchSize-1 }) + _, _ = collection.Attach(benchInts).IndexWhere(func(v int) bool { return v == benchSize-1 }) } } @@ -300,7 +300,7 @@ func benchEachCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { sum := 0 - collection.New(benchInts).Each(func(v int) { sum += v }) + collection.Attach(benchInts).Each(func(v int) { sum += v }) } } @@ -317,7 +317,7 @@ func benchMapCollection(b *testing.B) { for i := 0; i < b.N; i++ { copy(workA, benchInts) - _ = collection.New(workA).Map(func(v int) int { return v * 3 }) + _ = collection.Attach(workA).Map(func(v int) int { return v * 3 }) } } @@ -333,7 +333,7 @@ func benchMapLo(b *testing.B) { func benchReduceCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.New(benchInts).Reduce(0, func(acc, v int) int { return acc + v }) + _ = collection.Attach(benchInts).Reduce(0, func(acc, v int) int { return acc + v }) } } @@ -349,7 +349,7 @@ func benchFilterCollection(b *testing.B) { for i := 0; i < b.N; i++ { copy(workA, benchInts) - _ = collection.New(workA).Filter(func(v int) bool { return v%3 == 0 }) + _ = collection.Attach(workA).Filter(func(v int) bool { return v%3 == 0 }) } } @@ -365,7 +365,7 @@ func benchFilterLo(b *testing.B) { func benchChunkCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.New(benchInts).Chunk(benchChunkSize) + _ = collection.Attach(benchInts).Chunk(benchChunkSize) } } @@ -379,7 +379,7 @@ func benchChunkLo(b *testing.B) { func benchTakeCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.New(benchInts).Take(benchTakeN) + _ = collection.Attach(benchInts).Take(benchTakeN) } } @@ -393,7 +393,7 @@ func benchTakeLo(b *testing.B) { func benchContainsCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Contains(collection.New(benchInts), benchSize-1) + _ = collection.Contains(collection.Attach(benchInts), benchSize-1) } } @@ -407,7 +407,7 @@ func benchContainsLo(b *testing.B) { func benchFindCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.New(benchInts).FirstWhere(func(v int) bool { return v == benchSize-1 }) + _, _ = collection.Attach(benchInts).FirstWhere(func(v int) bool { return v == benchSize-1 }) } } @@ -421,7 +421,7 @@ func benchFindLo(b *testing.B) { func benchGroupByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.GroupBySlice(collection.New(benchInts), func(v int) int { return v % benchGroupByMod }) + _ = collection.GroupBySlice(collection.Attach(benchInts), func(v int) int { return v % benchGroupByMod }) } } @@ -435,7 +435,7 @@ func benchGroupByLo(b *testing.B) { func benchCountByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.CountBy(collection.New(benchIntsDup), func(v int) int { return v }) + _ = collection.CountBy(collection.Attach(benchIntsDup), func(v int) int { return v }) } } @@ -449,7 +449,7 @@ func benchCountByLo(b *testing.B) { func benchCountByValueCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.CountByValue(collection.New(benchIntsDup)) + _ = collection.CountByValue(collection.Attach(benchIntsDup)) } } @@ -463,7 +463,7 @@ func benchCountByValueLo(b *testing.B) { func benchSkipCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.New(benchInts).Skip(benchSkipN) + _ = collection.Attach(benchInts).Skip(benchSkipN) } } @@ -477,7 +477,7 @@ func benchSkipLo(b *testing.B) { func benchSkipLastCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.New(benchInts).SkipLast(benchSkipN) + _ = collection.Attach(benchInts).SkipLast(benchSkipN) } } @@ -493,7 +493,7 @@ func benchReverseCollection(b *testing.B) { for i := 0; i < b.N; i++ { copy(workA, benchInts) - _ = collection.New(workA).Reverse() + _ = collection.Attach(workA).Reverse() } } @@ -511,7 +511,7 @@ func benchShuffleCollection(b *testing.B) { for i := 0; i < b.N; i++ { copy(workA, benchInts) - _ = collection.New(workA).Shuffle() + _ = collection.Attach(workA).Shuffle() } } @@ -527,7 +527,7 @@ func benchShuffleLo(b *testing.B) { func benchZipCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Zip(collection.New(benchInts), collection.New(benchIntsDup)) + _ = collection.Zip(collection.Attach(benchInts), collection.Attach(benchIntsDup)) } } @@ -541,7 +541,7 @@ func benchZipLo(b *testing.B) { func benchZipWithCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.ZipWith(collection.New(benchInts), collection.New(benchIntsDup), func(a, b int) int { + _ = collection.ZipWith(collection.Attach(benchInts), collection.Attach(benchIntsDup), func(a, b int) int { return a + b }) } @@ -559,7 +559,7 @@ func benchZipWithLo(b *testing.B) { func benchUniqueCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.UniqueComparable(collection.New(benchIntsDup)) + _ = collection.UniqueComparable(collection.Attach(benchIntsDup)) } } @@ -573,7 +573,7 @@ func benchUniqueLo(b *testing.B) { func benchUniqueByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.UniqueBy(collection.New(benchIntsDup), func(v int) int { return v }) + _ = collection.UniqueBy(collection.Attach(benchIntsDup), func(v int) int { return v }) } } @@ -587,7 +587,7 @@ func benchUniqueByLo(b *testing.B) { func benchUnionCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Union(collection.New(unionLeft), collection.New(unionRight)) + _ = collection.Union(collection.Attach(unionLeft), collection.Attach(unionRight)) } } @@ -601,7 +601,7 @@ func benchUnionLo(b *testing.B) { func benchIntersectCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Intersect(collection.New(intersectLeft), collection.New(intersectRight)) + _ = collection.Intersect(collection.Attach(intersectLeft), collection.Attach(intersectRight)) } } @@ -615,7 +615,7 @@ func benchIntersectLo(b *testing.B) { func benchDifferenceCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Difference(collection.New(differenceLeft), collection.New(differenceRight)) + _ = collection.Difference(collection.Attach(differenceLeft), collection.Attach(differenceRight)) } } @@ -629,7 +629,7 @@ func benchDifferenceLo(b *testing.B) { func benchToMapCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.ToMap(collection.New(benchInts), func(v int) int { return v }, func(v int) int { return v }) + _ = collection.ToMap(collection.Attach(benchInts), func(v int) int { return v }, func(v int) int { return v }) } } @@ -643,7 +643,7 @@ func benchToMapLo(b *testing.B) { func benchSumCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.NewNumeric(benchInts).Sum() + _ = collection.AttachNumeric(benchInts).Sum() } } @@ -657,7 +657,7 @@ func benchSumLo(b *testing.B) { func benchMinCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.NewNumeric(benchInts).Min() + _, _ = collection.AttachNumeric(benchInts).Min() } } @@ -671,7 +671,7 @@ func benchMinLo(b *testing.B) { func benchMaxCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.NewNumeric(benchInts).Max() + _, _ = collection.AttachNumeric(benchInts).Max() } } @@ -696,7 +696,6 @@ func renderTable(results []benchResult) string { } var buf bytes.Buffer - buf.WriteString("### Performance Benchmarks\n\n") buf.WriteString("| Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) |\n") buf.WriteString("|---:|----------------|:--:|------------------|:--:|--------------------|\n") @@ -857,15 +856,52 @@ func replaceSection(readme, content string) (string, error) { return "", fmt.Errorf("benchmark anchors not found or malformed") } + section := readme[start+len(benchStart) : end] + updated, err := replaceBenchTable(section, content) + if err != nil { + return "", err + } + var buf bytes.Buffer buf.WriteString(readme[:start+len(benchStart)]) - buf.WriteString("\n\n") - buf.WriteString(content) - buf.WriteString("\n") + buf.WriteString(updated) buf.WriteString(readme[end:]) return buf.String(), nil } +func replaceBenchTable(section, table string) (string, error) { + lines := strings.Split(section, "\n") + tableStart := -1 + for i, line := range lines { + if strings.HasPrefix(strings.TrimSpace(line), "| Op |") { + tableStart = i + break + } + } + if tableStart == -1 { + return "", fmt.Errorf("benchmark table header not found") + } + + tableEnd := tableStart + for tableEnd < len(lines) { + line := strings.TrimSpace(lines[tableEnd]) + if line == "" { + break + } + tableEnd++ + } + + var buf bytes.Buffer + if tableStart > 0 { + buf.WriteString(strings.Join(lines[:tableStart], "\n")) + buf.WriteString("\n") + } + buf.WriteString(strings.TrimSpace(table)) + buf.WriteString("\n") + buf.WriteString(strings.Join(lines[tableEnd:], "\n")) + return buf.String(), nil +} + // ---------------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------------- From ec363d88aca77e09c7975a8cd9693031d684645b Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 20:56:15 -0600 Subject: [PATCH 05/30] docs: benchmarks --- README.md | 40 ++++++++++++ docs/bench/main.go | 153 ++++++++++++++++++++++++++++----------------- 2 files changed, 136 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 44a516f..9585878 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,46 @@ That design choice doesn't matter much for some single operations. It matters a +#### Operator-only (Attach) vs lo + +| Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | +|---:|----------------|:--:|------------------|:--:|--------------------| +| **All** | 234ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Any** | 232ns / 233ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Chunk** | 125ns / 1.0µs | **8.31x** | 1.3KB / 9.3KB | **7.25x less** | 1 / 51 | +| **Contains** | 242ns / 242ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **CountBy** | 8.6µs / 8.4µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 | +| **CountByValue** | 8.5µs / 8.7µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 | +| **Difference** | 19.1µs / 44.2µs | **2.32x** | 82.1KB / 108.8KB | **1.33x less** | 12 / 43 | +| **Each** | 237ns / 249ns | **1.05x** | 0B / 0B | ≈ | 0 / 0 | +| **Filter** | 636ns / 1.1µs | **1.78x** | 0B / 8.2KB | **∞x less** | 0 / 1 | +| **First** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | +| **FirstWhere** | 248ns / 236ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **GroupBySlice** | 8.1µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 83 / 83 | +| **IndexWhere** | 235ns / 238ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Intersect** | 11.1µs / 10.8µs | ≈ | 11.4KB / 11.4KB | ≈ | 20 / 19 | +| **Last** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | +| **Map** | 826ns / 806ns | ≈ | 8.2KB / 8.2KB | ≈ | 1 / 1 | +| **Max** | 229ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Min** | 229ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **None** | 234ns / 240ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Pipeline F→M→T→R** | 917ns / 1.3µs | **1.38x** | 4.1KB / 12.3KB | **2.98x less** | 2 / 2 | +| **Reduce (sum)** | 232ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Reverse** | 221ns / 236ns | **1.07x** | 0B / 0B | ≈ | 0 / 0 | +| **Shuffle** | 4.1µs / 5.4µs | **1.33x** | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Skip** | 0ns / 692ns | ∞ | 0B / 8.2KB | **∞x less** | 0 / 1 | +| **SkipLast** | 0ns / 693ns | ∞ | 0B / 8.2KB | **∞x less** | 0 / 1 | +| **Sum** | 231ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | +| **Take** | 11ns / 0ns | 0.00x | 24B / 0B | ∞x more | 1 / 0 | +| **ToMap** | 7.7µs / 7.9µs | ≈ | 36.9KB / 37.0KB | ≈ | 5 / 6 | +| **Union** | 17.2µs / 17.7µs | ≈ | 90.3KB / 90.3KB | ≈ | 11 / 10 | +| **Unique** | 6.6µs / 6.6µs | ≈ | 45.1KB / 45.1KB | ≈ | 6 / 6 | +| **UniqueBy** | 7.1µs / 6.8µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **Zip** | 1.4µs / 3.4µs | **2.40x** | 16.4KB / 16.4KB | ≈ | 1 / 1 | +| **ZipWith** | 1.0µs / 3.1µs | **3.01x** | 8.2KB / 8.2KB | ≈ | 1 / 1 | + +#### Safe-by-default (New) vs lo + | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| | **All** | 234ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | diff --git a/docs/bench/main.go b/docs/bench/main.go index d3b255a..e207761 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -35,10 +35,13 @@ func main() { flag.Parse() start := time.Now() - results := runBenches(parseOnly(*onlyFlag)) - table := renderTable(results) + only := parseOnly(*onlyFlag) + attachResults := runBenches(only, benchAttach) + newResults := runBenches(only, benchNew) + attachTable := renderTable(attachResults) + newTable := renderTable(newResults) - if err := updateReadme(table); err != nil { + if err := updateReadme(attachTable, newTable); err != nil { fmt.Println("Error:", err) os.Exit(1) } @@ -53,7 +56,31 @@ func main() { // Benchmark runner // ---------------------------------------------------------------------------- -func runBenches(only map[string]struct{}) []benchResult { +type benchMode string + +const ( + benchAttach benchMode = "attach" + benchNew benchMode = "new" +) + +var ( + ctorInts func([]int) *collection.Collection[int] + ctorNumericInt func([]int) *collection.NumericCollection[int] +) + +func setBenchMode(mode benchMode) { + switch mode { + case benchNew: + ctorInts = collection.New[int] + ctorNumericInt = collection.NewNumeric[int] + default: + ctorInts = collection.Attach[int] + ctorNumericInt = collection.AttachNumeric[int] + } +} + +func runBenches(only map[string]struct{}, mode benchMode) []benchResult { + setBenchMode(mode) cases := []struct { name string col func(*testing.B) @@ -192,7 +219,7 @@ func benchPipelineCollection(b *testing.B) { for i := 0; i < b.N; i++ { copy(workA, benchInts) - _ = collection.Attach(workA). + _ = ctorInts(workA). Filter(func(v int) bool { return v%2 == 0 }). Map(func(v int) int { return v * v }). Take(benchPipelineLen). @@ -215,7 +242,7 @@ func benchPipelineLo(b *testing.B) { func benchAllCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Attach(benchInts).All(func(v int) bool { return v < benchSize+1 }) + _ = ctorInts(benchInts).All(func(v int) bool { return v < benchSize+1 }) } } @@ -229,7 +256,7 @@ func benchAllLo(b *testing.B) { func benchAnyCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Attach(benchInts).Any(func(v int) bool { return v == benchSize-1 }) + _ = ctorInts(benchInts).Any(func(v int) bool { return v == benchSize-1 }) } } @@ -243,7 +270,7 @@ func benchAnyLo(b *testing.B) { func benchNoneCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Attach(benchInts).None(func(v int) bool { return v < 0 }) + _ = ctorInts(benchInts).None(func(v int) bool { return v < 0 }) } } @@ -257,7 +284,7 @@ func benchNoneLo(b *testing.B) { func benchFirstCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.Attach(benchInts).First() + _, _ = ctorInts(benchInts).First() } } @@ -271,7 +298,7 @@ func benchFirstLo(b *testing.B) { func benchLastCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.Attach(benchInts).Last() + _, _ = ctorInts(benchInts).Last() } } @@ -285,7 +312,7 @@ func benchLastLo(b *testing.B) { func benchIndexWhereCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.Attach(benchInts).IndexWhere(func(v int) bool { return v == benchSize-1 }) + _, _ = ctorInts(benchInts).IndexWhere(func(v int) bool { return v == benchSize-1 }) } } @@ -300,7 +327,7 @@ func benchEachCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { sum := 0 - collection.Attach(benchInts).Each(func(v int) { sum += v }) + ctorInts(benchInts).Each(func(v int) { sum += v }) } } @@ -317,7 +344,7 @@ func benchMapCollection(b *testing.B) { for i := 0; i < b.N; i++ { copy(workA, benchInts) - _ = collection.Attach(workA).Map(func(v int) int { return v * 3 }) + _ = ctorInts(workA).Map(func(v int) int { return v * 3 }) } } @@ -333,7 +360,7 @@ func benchMapLo(b *testing.B) { func benchReduceCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Attach(benchInts).Reduce(0, func(acc, v int) int { return acc + v }) + _ = ctorInts(benchInts).Reduce(0, func(acc, v int) int { return acc + v }) } } @@ -349,7 +376,7 @@ func benchFilterCollection(b *testing.B) { for i := 0; i < b.N; i++ { copy(workA, benchInts) - _ = collection.Attach(workA).Filter(func(v int) bool { return v%3 == 0 }) + _ = ctorInts(workA).Filter(func(v int) bool { return v%3 == 0 }) } } @@ -365,7 +392,7 @@ func benchFilterLo(b *testing.B) { func benchChunkCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Attach(benchInts).Chunk(benchChunkSize) + _ = ctorInts(benchInts).Chunk(benchChunkSize) } } @@ -379,7 +406,7 @@ func benchChunkLo(b *testing.B) { func benchTakeCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Attach(benchInts).Take(benchTakeN) + _ = ctorInts(benchInts).Take(benchTakeN) } } @@ -393,7 +420,7 @@ func benchTakeLo(b *testing.B) { func benchContainsCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Contains(collection.Attach(benchInts), benchSize-1) + _ = collection.Contains(ctorInts(benchInts), benchSize-1) } } @@ -407,7 +434,7 @@ func benchContainsLo(b *testing.B) { func benchFindCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.Attach(benchInts).FirstWhere(func(v int) bool { return v == benchSize-1 }) + _, _ = ctorInts(benchInts).FirstWhere(func(v int) bool { return v == benchSize-1 }) } } @@ -421,7 +448,7 @@ func benchFindLo(b *testing.B) { func benchGroupByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.GroupBySlice(collection.Attach(benchInts), func(v int) int { return v % benchGroupByMod }) + _ = collection.GroupBySlice(ctorInts(benchInts), func(v int) int { return v % benchGroupByMod }) } } @@ -435,7 +462,7 @@ func benchGroupByLo(b *testing.B) { func benchCountByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.CountBy(collection.Attach(benchIntsDup), func(v int) int { return v }) + _ = collection.CountBy(ctorInts(benchIntsDup), func(v int) int { return v }) } } @@ -449,7 +476,7 @@ func benchCountByLo(b *testing.B) { func benchCountByValueCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.CountByValue(collection.Attach(benchIntsDup)) + _ = collection.CountByValue(ctorInts(benchIntsDup)) } } @@ -463,7 +490,7 @@ func benchCountByValueLo(b *testing.B) { func benchSkipCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Attach(benchInts).Skip(benchSkipN) + _ = ctorInts(benchInts).Skip(benchSkipN) } } @@ -477,7 +504,7 @@ func benchSkipLo(b *testing.B) { func benchSkipLastCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Attach(benchInts).SkipLast(benchSkipN) + _ = ctorInts(benchInts).SkipLast(benchSkipN) } } @@ -493,7 +520,7 @@ func benchReverseCollection(b *testing.B) { for i := 0; i < b.N; i++ { copy(workA, benchInts) - _ = collection.Attach(workA).Reverse() + _ = ctorInts(workA).Reverse() } } @@ -511,7 +538,7 @@ func benchShuffleCollection(b *testing.B) { for i := 0; i < b.N; i++ { copy(workA, benchInts) - _ = collection.Attach(workA).Shuffle() + _ = ctorInts(workA).Shuffle() } } @@ -527,7 +554,7 @@ func benchShuffleLo(b *testing.B) { func benchZipCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Zip(collection.Attach(benchInts), collection.Attach(benchIntsDup)) + _ = collection.Zip(ctorInts(benchInts), ctorInts(benchIntsDup)) } } @@ -541,7 +568,7 @@ func benchZipLo(b *testing.B) { func benchZipWithCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.ZipWith(collection.Attach(benchInts), collection.Attach(benchIntsDup), func(a, b int) int { + _ = collection.ZipWith(ctorInts(benchInts), ctorInts(benchIntsDup), func(a, b int) int { return a + b }) } @@ -559,7 +586,7 @@ func benchZipWithLo(b *testing.B) { func benchUniqueCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.UniqueComparable(collection.Attach(benchIntsDup)) + _ = collection.UniqueComparable(ctorInts(benchIntsDup)) } } @@ -573,7 +600,7 @@ func benchUniqueLo(b *testing.B) { func benchUniqueByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.UniqueBy(collection.Attach(benchIntsDup), func(v int) int { return v }) + _ = collection.UniqueBy(ctorInts(benchIntsDup), func(v int) int { return v }) } } @@ -587,7 +614,7 @@ func benchUniqueByLo(b *testing.B) { func benchUnionCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Union(collection.Attach(unionLeft), collection.Attach(unionRight)) + _ = collection.Union(ctorInts(unionLeft), ctorInts(unionRight)) } } @@ -601,7 +628,7 @@ func benchUnionLo(b *testing.B) { func benchIntersectCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Intersect(collection.Attach(intersectLeft), collection.Attach(intersectRight)) + _ = collection.Intersect(ctorInts(intersectLeft), ctorInts(intersectRight)) } } @@ -615,7 +642,7 @@ func benchIntersectLo(b *testing.B) { func benchDifferenceCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Difference(collection.Attach(differenceLeft), collection.Attach(differenceRight)) + _ = collection.Difference(ctorInts(differenceLeft), ctorInts(differenceRight)) } } @@ -629,7 +656,7 @@ func benchDifferenceLo(b *testing.B) { func benchToMapCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.ToMap(collection.Attach(benchInts), func(v int) int { return v }, func(v int) int { return v }) + _ = collection.ToMap(ctorInts(benchInts), func(v int) int { return v }, func(v int) int { return v }) } } @@ -643,7 +670,7 @@ func benchToMapLo(b *testing.B) { func benchSumCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.AttachNumeric(benchInts).Sum() + _ = ctorNumericInt(benchInts).Sum() } } @@ -657,7 +684,7 @@ func benchSumLo(b *testing.B) { func benchMinCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.AttachNumeric(benchInts).Min() + _, _ = ctorNumericInt(benchInts).Min() } } @@ -671,7 +698,7 @@ func benchMinLo(b *testing.B) { func benchMaxCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = collection.AttachNumeric(benchInts).Max() + _, _ = ctorNumericInt(benchInts).Max() } } @@ -829,7 +856,7 @@ func formatInt(v int64) string { // README injection // ---------------------------------------------------------------------------- -func updateReadme(table string) error { +func updateReadme(attachTable, newTable string) error { root, err := findRoot() if err != nil { return err @@ -841,7 +868,7 @@ func updateReadme(table string) error { return err } - out, err := replaceSection(string(data), table) + out, err := replaceSection(string(data), attachTable, newTable) if err != nil { return err } @@ -849,7 +876,7 @@ func updateReadme(table string) error { return os.WriteFile(readmePath, []byte(out), 0o644) } -func replaceSection(readme, content string) (string, error) { +func replaceSection(readme, attachTable, newTable string) (string, error) { start := strings.Index(readme, benchStart) end := strings.Index(readme, benchEnd) if start == -1 || end == -1 || end < start { @@ -857,7 +884,7 @@ func replaceSection(readme, content string) (string, error) { } section := readme[start+len(benchStart) : end] - updated, err := replaceBenchTable(section, content) + updated, err := replaceBenchTables(section, attachTable, newTable) if err != nil { return "", err } @@ -869,36 +896,48 @@ func replaceSection(readme, content string) (string, error) { return buf.String(), nil } -func replaceBenchTable(section, table string) (string, error) { +func replaceBenchTables(section, attachTable, newTable string) (string, error) { lines := strings.Split(section, "\n") - tableStart := -1 + tableStarts := []int{} for i, line := range lines { if strings.HasPrefix(strings.TrimSpace(line), "| Op |") { - tableStart = i - break + tableStarts = append(tableStarts, i) } } - if tableStart == -1 { - return "", fmt.Errorf("benchmark table header not found") + if len(tableStarts) < 2 { + return "", fmt.Errorf("expected two benchmark tables in section") } - tableEnd := tableStart - for tableEnd < len(lines) { - line := strings.TrimSpace(lines[tableEnd]) - if line == "" { - break + tableEnds := make([]int, len(tableStarts)) + for i, start := range tableStarts { + end := start + for end < len(lines) { + line := strings.TrimSpace(lines[end]) + if line == "" { + break + } + end++ } - tableEnd++ + tableEnds[i] = end } + firstStart, firstEnd := tableStarts[0], tableEnds[0] + secondStart, secondEnd := tableStarts[1], tableEnds[1] + var buf bytes.Buffer - if tableStart > 0 { - buf.WriteString(strings.Join(lines[:tableStart], "\n")) + if firstStart > 0 { + buf.WriteString(strings.Join(lines[:firstStart], "\n")) + buf.WriteString("\n") + } + buf.WriteString(strings.TrimSpace(attachTable)) + buf.WriteString("\n") + buf.WriteString(strings.Join(lines[firstEnd:secondStart], "\n")) + if buf.Len() > 0 && !strings.HasSuffix(buf.String(), "\n") { buf.WriteString("\n") } - buf.WriteString(strings.TrimSpace(table)) + buf.WriteString(strings.TrimSpace(newTable)) buf.WriteString("\n") - buf.WriteString(strings.Join(lines[tableEnd:], "\n")) + buf.WriteString(strings.Join(lines[secondEnd:], "\n")) return buf.String(), nil } From 75591784d32f80fabb112a0d79f73970edd3366e Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 21:36:28 -0600 Subject: [PATCH 06/30] docs: benchmarks --- README.md | 133 +++++++------- docs/bench/main.go | 423 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 397 insertions(+), 159 deletions(-) diff --git a/README.md b/README.md index 9585878..1be5125 100644 --- a/README.md +++ b/README.md @@ -95,77 +95,76 @@ That design choice doesn't matter much for some single operations. It matters a | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 234ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Any** | 232ns / 233ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Chunk** | 125ns / 1.0µs | **8.31x** | 1.3KB / 9.3KB | **7.25x less** | 1 / 51 | -| **Contains** | 242ns / 242ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **CountBy** | 8.6µs / 8.4µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 | -| **CountByValue** | 8.5µs / 8.7µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 | -| **Difference** | 19.1µs / 44.2µs | **2.32x** | 82.1KB / 108.8KB | **1.33x less** | 12 / 43 | -| **Each** | 237ns / 249ns | **1.05x** | 0B / 0B | ≈ | 0 / 0 | -| **Filter** | 636ns / 1.1µs | **1.78x** | 0B / 8.2KB | **∞x less** | 0 / 1 | -| **First** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | -| **FirstWhere** | 248ns / 236ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **GroupBySlice** | 8.1µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 83 / 83 | -| **IndexWhere** | 235ns / 238ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Intersect** | 11.1µs / 10.8µs | ≈ | 11.4KB / 11.4KB | ≈ | 20 / 19 | -| **Last** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | -| **Map** | 826ns / 806ns | ≈ | 8.2KB / 8.2KB | ≈ | 1 / 1 | -| **Max** | 229ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Min** | 229ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **None** | 234ns / 240ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Pipeline F→M→T→R** | 917ns / 1.3µs | **1.38x** | 4.1KB / 12.3KB | **2.98x less** | 2 / 2 | -| **Reduce (sum)** | 232ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Reverse** | 221ns / 236ns | **1.07x** | 0B / 0B | ≈ | 0 / 0 | -| **Shuffle** | 4.1µs / 5.4µs | **1.33x** | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Skip** | 0ns / 692ns | ∞ | 0B / 8.2KB | **∞x less** | 0 / 1 | -| **SkipLast** | 0ns / 693ns | ∞ | 0B / 8.2KB | **∞x less** | 0 / 1 | -| **Sum** | 231ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Take** | 11ns / 0ns | 0.00x | 24B / 0B | ∞x more | 1 / 0 | -| **ToMap** | 7.7µs / 7.9µs | ≈ | 36.9KB / 37.0KB | ≈ | 5 / 6 | -| **Union** | 17.2µs / 17.7µs | ≈ | 90.3KB / 90.3KB | ≈ | 11 / 10 | -| **Unique** | 6.6µs / 6.6µs | ≈ | 45.1KB / 45.1KB | ≈ | 6 / 6 | -| **UniqueBy** | 7.1µs / 6.8µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **Zip** | 1.4µs / 3.4µs | **2.40x** | 16.4KB / 16.4KB | ≈ | 1 / 1 | -| **ZipWith** | 1.0µs / 3.1µs | **3.01x** | 8.2KB / 8.2KB | ≈ | 1 / 1 | +| **All** | 250ns / 234ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 249ns / 234ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 136ns / 1.0µs | **7.60x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 247ns / 233ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.0µs / 7.9µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.1µs / 8.2µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 19.0µs / 43.0µs | **2.27x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 251ns / 230ns | 0.92x | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 639ns / 1.0µs | **1.63x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **First** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | +| **FirstWhere** | 247ns / 232ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 7.9µs / 8.0µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 247ns / 233ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 10.9µs / 10.6µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **Last** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | +| **Map** | 833ns / 794ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **Max** | 253ns / 230ns | 0.91x | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 252ns / 235ns | 0.93x | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 249ns / 232ns | 0.93x | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 718ns / 1.3µs | **1.77x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | +| **Reduce (sum)** | 251ns / 231ns | 0.92x | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 228ns / 226ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 3.8µs / 5.3µs | **1.37x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 11ns / 683ns | **62.08x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 11ns / 685ns | **62.95x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 262ns / 231ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 21ns / 0ns | 0.02x | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.5µs / 7.7µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 17.1µs / 17.4µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Unique** | 6.3µs / 6.2µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **UniqueBy** | 6.6µs / 6.3µs | 0.94x | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.4µs / 3.1µs | **2.26x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 988ns / 2.9µs | **2.93x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | #### Safe-by-default (New) vs lo - | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 234ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Any** | 232ns / 233ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Chunk** | 125ns / 1.0µs | **8.31x** | 1.3KB / 9.3KB | **7.25x less** | 1 / 51 | -| **Contains** | 242ns / 242ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **CountBy** | 8.6µs / 8.4µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 | -| **CountByValue** | 8.5µs / 8.7µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 | -| **Difference** | 19.1µs / 44.2µs | **2.32x** | 82.1KB / 108.8KB | **1.33x less** | 12 / 43 | -| **Each** | 237ns / 249ns | **1.05x** | 0B / 0B | ≈ | 0 / 0 | -| **Filter** | 636ns / 1.1µs | **1.78x** | 0B / 8.2KB | **∞x less** | 0 / 1 | -| **First** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | -| **FirstWhere** | 248ns / 236ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **GroupBySlice** | 8.1µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 83 / 83 | -| **IndexWhere** | 235ns / 238ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Intersect** | 11.1µs / 10.8µs | ≈ | 11.4KB / 11.4KB | ≈ | 20 / 19 | -| **Last** | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 | -| **Map** | 826ns / 806ns | ≈ | 8.2KB / 8.2KB | ≈ | 1 / 1 | -| **Max** | 229ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Min** | 229ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **None** | 234ns / 240ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Pipeline F→M→T→R** | 917ns / 1.3µs | **1.38x** | 4.1KB / 12.3KB | **2.98x less** | 2 / 2 | -| **Reduce (sum)** | 232ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Reverse** | 221ns / 236ns | **1.07x** | 0B / 0B | ≈ | 0 / 0 | -| **Shuffle** | 4.1µs / 5.4µs | **1.33x** | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Skip** | 0ns / 692ns | ∞ | 0B / 8.2KB | **∞x less** | 0 / 1 | -| **SkipLast** | 0ns / 693ns | ∞ | 0B / 8.2KB | **∞x less** | 0 / 1 | -| **Sum** | 231ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 | -| **Take** | 11ns / 0ns | 0.00x | 24B / 0B | ∞x more | 1 / 0 | -| **ToMap** | 7.7µs / 7.9µs | ≈ | 36.9KB / 37.0KB | ≈ | 5 / 6 | -| **Union** | 17.2µs / 17.7µs | ≈ | 90.3KB / 90.3KB | ≈ | 11 / 10 | -| **Unique** | 6.6µs / 6.6µs | ≈ | 45.1KB / 45.1KB | ≈ | 6 / 6 | -| **UniqueBy** | 7.1µs / 6.8µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **Zip** | 1.4µs / 3.4µs | **2.40x** | 16.4KB / 16.4KB | ≈ | 1 / 1 | -| **ZipWith** | 1.0µs / 3.1µs | **3.01x** | 8.2KB / 8.2KB | ≈ | 1 / 1 | +| **All** | 818ns / 774ns | 0.95x | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Any** | 818ns / 777ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Chunk** | 775ns / 1.7µs | **2.19x** | 9.5KB / 17.5KB | **1.84x less** | 3 / 53 | +| **Contains** | 820ns / 782ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **CountBy** | 8.8µs / 8.6µs | ≈ | 17.6KB / 17.6KB | ≈ | 13 / 13 | +| **CountByValue** | 8.8µs / 8.6µs | ≈ | 17.6KB / 17.6KB | ≈ | 13 / 13 | +| **Difference** | 20.3µs / 44.6µs | **2.20x** | 98.5KB / 125.3KB | **1.27x less** | 16 / 47 | +| **Each** | 809ns / 777ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Filter** | 1.1µs / 1.6µs | **1.50x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | +| **First** | 662ns / 660ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **FirstWhere** | 822ns / 782ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **GroupBySlice** | 8.6µs / 8.6µs | ≈ | 29.2KB / 29.2KB | ≈ | 85 / 85 | +| **IndexWhere** | 812ns / 778ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Intersect** | 12.0µs / 11.7µs | ≈ | 27.8KB / 27.8KB | ≈ | 24 / 23 | +| **Last** | 660ns / 661ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Map** | 1.4µs / 1.4µs | ≈ | 16.4KB / 16.4KB | ≈ | 3 / 3 | +| **Max** | 832ns / 798ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 2 | +| **Min** | 842ns / 787ns | 0.94x | 8.2KB / 8.2KB | ≈ | 3 / 2 | +| **None** | 818ns / 772ns | 0.94x | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Pipeline F→M→T→R** | 1.2µs / 1.8µs | **1.49x** | 12.3KB / 20.5KB | **1.66x less** | 4 / 4 | +| **Reduce (sum)** | 811ns / 780ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Reverse** | 748ns / 743ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Shuffle** | 4.3µs / 5.5µs | **1.27x** | 16.4KB / 8.2KB | 0.50x more | 4 / 2 | +| **Skip** | 667ns / 1.4µs | **2.03x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | +| **SkipLast** | 665ns / 1.4µs | **2.04x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | +| **Sum** | 837ns / 791ns | 0.94x | 8.2KB / 8.2KB | ≈ | 3 / 2 | +| **Take** | 667ns / 668ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 2 | +| **ToMap** | 8.1µs / 8.5µs | **1.05x** | 45.2KB / 45.2KB | ≈ | 7 / 8 | +| **Union** | 18.3µs / 18.7µs | ≈ | 106.7KB / 106.7KB | ≈ | 15 / 14 | +| **Unique** | 7.0µs / 6.8µs | ≈ | 53.4KB / 53.4KB | ≈ | 8 / 8 | +| **UniqueBy** | 7.3µs / 7.0µs | ≈ | 53.4KB / 53.4KB | ≈ | 9 / 8 | +| **Zip** | 2.6µs / 4.4µs | **1.69x** | 32.8KB / 32.8KB | ≈ | 5 / 5 | +| **ZipWith** | 2.2µs / 4.1µs | **1.83x** | 24.6KB / 24.6KB | ≈ | 5 / 5 | ## How to read the benchmarks diff --git a/docs/bench/main.go b/docs/bench/main.go index e207761..63d1734 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -20,6 +20,8 @@ const ( benchEnd = "" hotPathIters = 10_000 + + benchInner = 8 ) type benchResult struct { @@ -66,9 +68,12 @@ const ( var ( ctorInts func([]int) *collection.Collection[int] ctorNumericInt func([]int) *collection.NumericCollection[int] + currentMode benchMode + loWrapperSink []byte ) func setBenchMode(mode benchMode) { + currentMode = mode switch mode { case benchNew: ctorInts = collection.New[int] @@ -156,12 +161,21 @@ func measure(name, impl string, fn func(*testing.B)) benchResult { fn(b) }) + nsPerOp := float64(res.NsPerOp()) + bytesPerOp := res.AllocedBytesPerOp() + allocsPerOp := res.AllocsPerOp() + if benchInner > 1 { + nsPerOp = nsPerOp / float64(benchInner) + bytesPerOp = bytesPerOp / int64(benchInner) + allocsPerOp = allocsPerOp / int64(benchInner) + } + return benchResult{ name: name, impl: impl, - nsPerOp: float64(res.NsPerOp()), - bytesPerOp: res.AllocedBytesPerOp(), - allocsPerOp: res.AllocsPerOp(), + nsPerOp: nsPerOp, + bytesPerOp: bytesPerOp, + allocsPerOp: allocsPerOp, } } @@ -217,495 +231,684 @@ func init() { func benchPipelineCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - copy(workA, benchInts) + for j := 0; j < benchInner; j++ { + input := collectionInputCopied(benchInts) + _ = ctorInts(input). + Filter(func(v int) bool { return v%2 == 0 }). + Map(func(v int) int { return v * v }). + Take(benchPipelineLen). + Reduce(0, func(acc, v int) int { return acc + v }) - _ = ctorInts(workA). - Filter(func(v int) bool { return v%2 == 0 }). - Map(func(v int) int { return v * v }). - Take(benchPipelineLen). - Reduce(0, func(acc, v int) int { return acc + v }) + } } } func benchPipelineLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - copy(workB, benchInts) + for j := 0; j < benchInner; j++ { + input := loInputCopied(benchInts) + + out := lo.Filter(input, func(v int, _ int) bool { return v%2 == 0 }) + out2 := lo.Map(out, func(v int, _ int) int { return v * v }) + out3 := lo.Subset(out2, 0, benchPipelineLen) + _ = lo.Reduce(out3, func(acc int, v int, _ int) int { return acc + v }, 0) - out := lo.Filter(workB, func(v int, _ int) bool { return v%2 == 0 }) - out2 := lo.Map(out, func(v int, _ int) int { return v * v }) - out3 := lo.Subset(out2, 0, benchPipelineLen) - _ = lo.Reduce(out3, func(acc int, v int, _ int) int { return acc + v }, 0) + } } } func benchAllCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = ctorInts(benchInts).All(func(v int) bool { return v < benchSize+1 }) + for j := 0; j < benchInner; j++ { + _ = ctorInts(benchInts).All(func(v int) bool { return v < benchSize+1 }) + + } } } func benchAllLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.EveryBy(benchInts, func(v int) bool { return v < benchSize+1 }) + for j := 0; j < benchInner; j++ { + _ = lo.EveryBy(loInput(benchInts), func(v int) bool { return v < benchSize+1 }) + + } } } func benchAnyCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = ctorInts(benchInts).Any(func(v int) bool { return v == benchSize-1 }) + for j := 0; j < benchInner; j++ { + _ = ctorInts(benchInts).Any(func(v int) bool { return v == benchSize-1 }) + + } } } func benchAnyLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.SomeBy(benchInts, func(v int) bool { return v == benchSize-1 }) + for j := 0; j < benchInner; j++ { + _ = lo.SomeBy(loInput(benchInts), func(v int) bool { return v == benchSize-1 }) + + } } } func benchNoneCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = ctorInts(benchInts).None(func(v int) bool { return v < 0 }) + for j := 0; j < benchInner; j++ { + _ = ctorInts(benchInts).None(func(v int) bool { return v < 0 }) + + } } } func benchNoneLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.NoneBy(benchInts, func(v int) bool { return v < 0 }) + for j := 0; j < benchInner; j++ { + _ = lo.NoneBy(loInput(benchInts), func(v int) bool { return v < 0 }) + + } } } func benchFirstCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = ctorInts(benchInts).First() + for j := 0; j < benchInner; j++ { + _, _ = ctorInts(benchInts).First() + + } } } func benchFirstLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = lo.First(benchInts) + for j := 0; j < benchInner; j++ { + _, _ = lo.First(loInput(benchInts)) + + } } } func benchLastCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = ctorInts(benchInts).Last() + for j := 0; j < benchInner; j++ { + _, _ = ctorInts(benchInts).Last() + + } } } func benchLastLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = lo.Last(benchInts) + for j := 0; j < benchInner; j++ { + _, _ = lo.Last(loInput(benchInts)) + + } } } func benchIndexWhereCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = ctorInts(benchInts).IndexWhere(func(v int) bool { return v == benchSize-1 }) + for j := 0; j < benchInner; j++ { + _, _ = ctorInts(benchInts).IndexWhere(func(v int) bool { return v == benchSize-1 }) + + } } } func benchIndexWhereLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _, _ = lo.FindIndexOf(benchInts, func(v int) bool { return v == benchSize-1 }) + for j := 0; j < benchInner; j++ { + _, _, _ = lo.FindIndexOf(loInput(benchInts), func(v int) bool { return v == benchSize-1 }) + + } } } func benchEachCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - sum := 0 - ctorInts(benchInts).Each(func(v int) { sum += v }) + for j := 0; j < benchInner; j++ { + sum := 0 + ctorInts(benchInts).Each(func(v int) { sum += v }) + + } } } func benchEachLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - sum := 0 - lo.ForEach(benchInts, func(v int, _ int) { sum += v }) + for j := 0; j < benchInner; j++ { + sum := 0 + lo.ForEach(loInput(benchInts), func(v int, _ int) { sum += v }) + + } } } func benchMapCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - copy(workA, benchInts) + for j := 0; j < benchInner; j++ { + input := collectionInputCopied(benchInts) + _ = ctorInts(input).Map(func(v int) int { return v * 3 }) - _ = ctorInts(workA).Map(func(v int) int { return v * 3 }) + } } } func benchMapLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - copy(workB, benchInts) + for j := 0; j < benchInner; j++ { + input := loInputCopied(benchInts) + _ = lo.Map(input, func(v int, _ int) int { return v * 3 }) - _ = lo.Map(workB, func(v int, _ int) int { return v * 3 }) + } } } func benchReduceCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = ctorInts(benchInts).Reduce(0, func(acc, v int) int { return acc + v }) + for j := 0; j < benchInner; j++ { + _ = ctorInts(benchInts).Reduce(0, func(acc, v int) int { return acc + v }) + + } } } func benchReduceLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Reduce(benchInts, func(acc int, v int, _ int) int { return acc + v }, 0) + for j := 0; j < benchInner; j++ { + _ = lo.Reduce(loInput(benchInts), func(acc int, v int, _ int) int { return acc + v }, 0) + + } } } func benchFilterCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - copy(workA, benchInts) + for j := 0; j < benchInner; j++ { + input := collectionInputCopied(benchInts) + _ = ctorInts(input).Filter(func(v int) bool { return v%3 == 0 }) - _ = ctorInts(workA).Filter(func(v int) bool { return v%3 == 0 }) + } } } func benchFilterLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - copy(workB, benchInts) + for j := 0; j < benchInner; j++ { + input := loInputCopied(benchInts) + _ = lo.Filter(input, func(v int, _ int) bool { return v%3 == 0 }) - _ = lo.Filter(workB, func(v int, _ int) bool { return v%3 == 0 }) + } } } func benchChunkCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = ctorInts(benchInts).Chunk(benchChunkSize) + for j := 0; j < benchInner; j++ { + _ = ctorInts(benchInts).Chunk(benchChunkSize) + + } } } func benchChunkLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Chunk(benchInts, benchChunkSize) + for j := 0; j < benchInner; j++ { + _ = lo.Chunk(loInput(benchInts), benchChunkSize) + + } } } func benchTakeCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = ctorInts(benchInts).Take(benchTakeN) + for j := 0; j < benchInner; j++ { + _ = ctorInts(benchInts).Take(benchTakeN) + + } } } func benchTakeLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Subset(benchInts, 0, uint(benchTakeN)) + for j := 0; j < benchInner; j++ { + _ = lo.Subset(loInput(benchInts), 0, uint(benchTakeN)) + + } } } func benchContainsCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Contains(ctorInts(benchInts), benchSize-1) + for j := 0; j < benchInner; j++ { + _ = collection.Contains(ctorInts(benchInts), benchSize-1) + + } } } func benchContainsLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.ContainsBy(benchInts, func(v int) bool { return v == benchSize-1 }) + for j := 0; j < benchInner; j++ { + _ = lo.ContainsBy(loInput(benchInts), func(v int) bool { return v == benchSize-1 }) + + } } } func benchFindCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = ctorInts(benchInts).FirstWhere(func(v int) bool { return v == benchSize-1 }) + for j := 0; j < benchInner; j++ { + _, _ = ctorInts(benchInts).FirstWhere(func(v int) bool { return v == benchSize-1 }) + + } } } func benchFindLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = lo.Find(benchInts, func(v int) bool { return v == benchSize-1 }) + for j := 0; j < benchInner; j++ { + _, _ = lo.Find(loInput(benchInts), func(v int) bool { return v == benchSize-1 }) + + } } } func benchGroupByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.GroupBySlice(ctorInts(benchInts), func(v int) int { return v % benchGroupByMod }) + for j := 0; j < benchInner; j++ { + _ = collection.GroupBySlice(ctorInts(benchInts), func(v int) int { return v % benchGroupByMod }) + + } } } func benchGroupByLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.GroupBy(benchInts, func(v int) int { return v % benchGroupByMod }) + for j := 0; j < benchInner; j++ { + _ = lo.GroupBy(loInput(benchInts), func(v int) int { return v % benchGroupByMod }) + + } } } func benchCountByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.CountBy(ctorInts(benchIntsDup), func(v int) int { return v }) + for j := 0; j < benchInner; j++ { + _ = collection.CountBy(ctorInts(benchIntsDup), func(v int) int { return v }) + + } } } func benchCountByLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.CountValuesBy(benchIntsDup, func(v int) int { return v }) + for j := 0; j < benchInner; j++ { + _ = lo.CountValuesBy(loInput(benchIntsDup), func(v int) int { return v }) + + } } } func benchCountByValueCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.CountByValue(ctorInts(benchIntsDup)) + for j := 0; j < benchInner; j++ { + _ = collection.CountByValue(ctorInts(benchIntsDup)) + + } } } func benchCountByValueLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.CountValues(benchIntsDup) + for j := 0; j < benchInner; j++ { + _ = lo.CountValues(loInput(benchIntsDup)) + + } } } func benchSkipCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = ctorInts(benchInts).Skip(benchSkipN) + for j := 0; j < benchInner; j++ { + _ = ctorInts(benchInts).Skip(benchSkipN) + + } } } func benchSkipLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Drop(benchInts, benchSkipN) + for j := 0; j < benchInner; j++ { + _ = lo.Drop(loInput(benchInts), benchSkipN) + + } } } func benchSkipLastCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = ctorInts(benchInts).SkipLast(benchSkipN) + for j := 0; j < benchInner; j++ { + _ = ctorInts(benchInts).SkipLast(benchSkipN) + + } } } func benchSkipLastLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.DropRight(benchInts, benchSkipN) + for j := 0; j < benchInner; j++ { + _ = lo.DropRight(loInput(benchInts), benchSkipN) + + } } } func benchReverseCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - copy(workA, benchInts) + for j := 0; j < benchInner; j++ { + input := collectionInputCopied(benchInts) + _ = ctorInts(input).Reverse() - _ = ctorInts(workA).Reverse() + } } } func benchReverseLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - copy(workB, benchInts) + for j := 0; j < benchInner; j++ { + input := loInputCopied(benchInts) + _ = lo.Reverse(input) - _ = lo.Reverse(workB) + } } } func benchShuffleCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - copy(workA, benchInts) + for j := 0; j < benchInner; j++ { + input := collectionInputCopied(benchInts) + _ = ctorInts(input).Shuffle() - _ = ctorInts(workA).Shuffle() + } } } func benchShuffleLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - copy(workB, benchInts) + for j := 0; j < benchInner; j++ { + input := loInputCopied(benchInts) + _ = lo.Shuffle(input) - _ = lo.Shuffle(workB) + } } } func benchZipCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Zip(ctorInts(benchInts), ctorInts(benchIntsDup)) + for j := 0; j < benchInner; j++ { + _ = collection.Zip(ctorInts(benchInts), ctorInts(benchIntsDup)) + + } } } func benchZipLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Zip2(benchInts, benchIntsDup) + for j := 0; j < benchInner; j++ { + _ = lo.Zip2(loInput(benchInts), loInput(benchIntsDup)) + + } } } func benchZipWithCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.ZipWith(ctorInts(benchInts), ctorInts(benchIntsDup), func(a, b int) int { - return a + b - }) + for j := 0; j < benchInner; j++ { + _ = collection.ZipWith(ctorInts(benchInts), ctorInts(benchIntsDup), func(a, b int) int { + return a + b + }) + + } } } func benchZipWithLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.ZipBy2(benchInts, benchIntsDup, func(a, b int) int { - return a + b - }) + for j := 0; j < benchInner; j++ { + _ = lo.ZipBy2(loInput(benchInts), loInput(benchIntsDup), func(a, b int) int { + return a + b + }) + + } } } func benchUniqueCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.UniqueComparable(ctorInts(benchIntsDup)) + for j := 0; j < benchInner; j++ { + _ = collection.UniqueComparable(ctorInts(benchIntsDup)) + + } } } func benchUniqueLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Uniq(benchIntsDup) + for j := 0; j < benchInner; j++ { + _ = lo.Uniq(loInput(benchIntsDup)) + + } } } func benchUniqueByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.UniqueBy(ctorInts(benchIntsDup), func(v int) int { return v }) + for j := 0; j < benchInner; j++ { + _ = collection.UniqueBy(ctorInts(benchIntsDup), func(v int) int { return v }) + + } } } func benchUniqueByLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.UniqBy(benchIntsDup, func(v int) int { return v }) + for j := 0; j < benchInner; j++ { + _ = lo.UniqBy(loInput(benchIntsDup), func(v int) int { return v }) + + } } } func benchUnionCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Union(ctorInts(unionLeft), ctorInts(unionRight)) + for j := 0; j < benchInner; j++ { + _ = collection.Union(ctorInts(unionLeft), ctorInts(unionRight)) + + } } } func benchUnionLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Union(unionLeft, unionRight) + for j := 0; j < benchInner; j++ { + _ = lo.Union(loInput(unionLeft), loInput(unionRight)) + + } } } func benchIntersectCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Intersect(ctorInts(intersectLeft), ctorInts(intersectRight)) + for j := 0; j < benchInner; j++ { + _ = collection.Intersect(ctorInts(intersectLeft), ctorInts(intersectRight)) + + } } } func benchIntersectLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Intersect(intersectLeft, intersectRight) + for j := 0; j < benchInner; j++ { + _ = lo.Intersect(loInput(intersectLeft), loInput(intersectRight)) + + } } } func benchDifferenceCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.Difference(ctorInts(differenceLeft), ctorInts(differenceRight)) + for j := 0; j < benchInner; j++ { + _ = collection.Difference(ctorInts(differenceLeft), ctorInts(differenceRight)) + + } } } func benchDifferenceLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = lo.Difference(differenceLeft, differenceRight) + for j := 0; j < benchInner; j++ { + _, _ = lo.Difference(loInput(differenceLeft), loInput(differenceRight)) + + } } } func benchToMapCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = collection.ToMap(ctorInts(benchInts), func(v int) int { return v }, func(v int) int { return v }) + for j := 0; j < benchInner; j++ { + _ = collection.ToMap(ctorInts(benchInts), func(v int) int { return v }, func(v int) int { return v }) + + } } } func benchToMapLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.SliceToMap(benchInts, func(v int) (int, int) { return v, v }) + for j := 0; j < benchInner; j++ { + _ = lo.SliceToMap(loInput(benchInts), func(v int) (int, int) { return v, v }) + + } } } func benchSumCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = ctorNumericInt(benchInts).Sum() + for j := 0; j < benchInner; j++ { + _ = ctorNumericInt(benchInts).Sum() + + } } } func benchSumLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Sum(benchInts) + for j := 0; j < benchInner; j++ { + _ = lo.Sum(loInput(benchInts)) + + } } } func benchMinCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = ctorNumericInt(benchInts).Min() + for j := 0; j < benchInner; j++ { + _, _ = ctorNumericInt(benchInts).Min() + + } } } func benchMinLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Min(benchInts) + for j := 0; j < benchInner; j++ { + _ = lo.Min(loInput(benchInts)) + + } } } func benchMaxCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = ctorNumericInt(benchInts).Max() + for j := 0; j < benchInner; j++ { + _, _ = ctorNumericInt(benchInts).Max() + + } } } func benchMaxLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = lo.Max(benchInts) + for j := 0; j < benchInner; j++ { + _ = lo.Max(loInput(benchInts)) + + } } } @@ -852,6 +1055,42 @@ func formatInt(v int64) string { } } +func collectionInputCopied(src []int) []int { + if currentMode == benchAttach { + copy(workA, src) + return workA + } + return src +} + +func loInputCopied(src []int) []int { + if currentMode == benchNew { + loWrapAlloc() + cp := make([]int, len(src)) + copy(cp, src) + return cp + } + copy(workB, src) + return workB +} + +func loInput(src []int) []int { + if currentMode == benchNew { + loWrapAlloc() + cp := make([]int, len(src)) + copy(cp, src) + return cp + } + return src +} + +func loWrapAlloc() { + if currentMode != benchNew { + return + } + loWrapperSink = make([]byte, 24) +} + // ---------------------------------------------------------------------------- // README injection // ---------------------------------------------------------------------------- From 454cce509b066bfd44a7c3a80bf30aa234bdc968 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 21:44:24 -0600 Subject: [PATCH 07/30] docs: benchmarks --- README.md | 126 ++++++++++++++++++++++----------------------- docs/bench/main.go | 2 +- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 1be5125..c35c0aa 100644 --- a/README.md +++ b/README.md @@ -95,76 +95,76 @@ That design choice doesn't matter much for some single operations. It matters a | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 250ns / 234ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 249ns / 234ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 136ns / 1.0µs | **7.60x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 247ns / 233ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.0µs / 7.9µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.1µs / 8.2µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 19.0µs / 43.0µs | **2.27x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | -| **Each** | 251ns / 230ns | 0.92x | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 639ns / 1.0µs | **1.63x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **All** | 249ns / 233ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 249ns / 250ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 145ns / 1.1µs | **7.31x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 249ns / 234ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.0µs / 8.1µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.2µs / 8.3µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 19.0µs / 43.3µs | **2.28x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 255ns / 234ns | 0.92x | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 641ns / 1.1µs | **1.66x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | | **First** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 247ns / 232ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 7.9µs / 8.0µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 247ns / 233ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 10.9µs / 10.6µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **FirstWhere** | 249ns / 234ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 8.0µs / 8.4µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 249ns / 235ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.0µs / 10.8µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | | **Last** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 833ns / 794ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **Max** | 253ns / 230ns | 0.91x | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 252ns / 235ns | 0.93x | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 249ns / 232ns | 0.93x | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 718ns / 1.3µs | **1.77x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | -| **Reduce (sum)** | 251ns / 231ns | 0.92x | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 228ns / 226ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 3.8µs / 5.3µs | **1.37x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 11ns / 683ns | **62.08x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 11ns / 685ns | **62.95x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 262ns / 231ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 21ns / 0ns | 0.02x | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.5µs / 7.7µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 17.1µs / 17.4µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | -| **Unique** | 6.3µs / 6.2µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.6µs / 6.3µs | 0.94x | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.4µs / 3.1µs | **2.26x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 988ns / 2.9µs | **2.93x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **Map** | 845ns / 809ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **Max** | 255ns / 234ns | 0.92x | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 255ns / 234ns | 0.92x | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 249ns / 233ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 725ns / 1.3µs | **1.77x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | +| **Reduce (sum)** | 254ns / 234ns | 0.92x | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 227ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 3.9µs / 5.3µs | **1.36x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 11ns / 704ns | **63.98x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 11ns / 700ns | **63.63x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 264ns / 234ns | 0.89x | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 22ns / 0ns | 0.02x | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.5µs / 7.8µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 17.1µs / 17.6µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Unique** | 6.4µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **UniqueBy** | 6.7µs / 6.3µs | 0.95x | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.4µs / 3.1µs | **2.22x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.0µs / 2.9µs | **2.91x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | #### Safe-by-default (New) vs lo | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 818ns / 774ns | 0.95x | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Any** | 818ns / 777ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Chunk** | 775ns / 1.7µs | **2.19x** | 9.5KB / 17.5KB | **1.84x less** | 3 / 53 | -| **Contains** | 820ns / 782ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **CountBy** | 8.8µs / 8.6µs | ≈ | 17.6KB / 17.6KB | ≈ | 13 / 13 | -| **CountByValue** | 8.8µs / 8.6µs | ≈ | 17.6KB / 17.6KB | ≈ | 13 / 13 | -| **Difference** | 20.3µs / 44.6µs | **2.20x** | 98.5KB / 125.3KB | **1.27x less** | 16 / 47 | -| **Each** | 809ns / 777ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Filter** | 1.1µs / 1.6µs | **1.50x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | -| **First** | 662ns / 660ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **FirstWhere** | 822ns / 782ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **GroupBySlice** | 8.6µs / 8.6µs | ≈ | 29.2KB / 29.2KB | ≈ | 85 / 85 | -| **IndexWhere** | 812ns / 778ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Intersect** | 12.0µs / 11.7µs | ≈ | 27.8KB / 27.8KB | ≈ | 24 / 23 | -| **Last** | 660ns / 661ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **All** | 830ns / 788ns | 0.95x | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Any** | 828ns / 787ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Chunk** | 794ns / 1.7µs | **2.18x** | 9.5KB / 17.5KB | **1.84x less** | 3 / 53 | +| **Contains** | 832ns / 795ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **CountBy** | 8.7µs / 8.7µs | ≈ | 17.6KB / 17.6KB | ≈ | 13 / 13 | +| **CountByValue** | 8.9µs / 8.7µs | ≈ | 17.6KB / 17.6KB | ≈ | 13 / 13 | +| **Difference** | 20.2µs / 44.8µs | **2.22x** | 98.5KB / 125.3KB | **1.27x less** | 16 / 47 | +| **Each** | 819ns / 794ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Filter** | 1.1µs / 1.6µs | **1.48x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | +| **First** | 670ns / 669ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **FirstWhere** | 832ns / 791ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **GroupBySlice** | 8.7µs / 9.1µs | ≈ | 29.2KB / 29.2KB | ≈ | 85 / 85 | +| **IndexWhere** | 827ns / 788ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Intersect** | 12.1µs / 11.9µs | ≈ | 27.8KB / 27.8KB | ≈ | 24 / 23 | +| **Last** | 670ns / 667ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | | **Map** | 1.4µs / 1.4µs | ≈ | 16.4KB / 16.4KB | ≈ | 3 / 3 | -| **Max** | 832ns / 798ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 2 | -| **Min** | 842ns / 787ns | 0.94x | 8.2KB / 8.2KB | ≈ | 3 / 2 | -| **None** | 818ns / 772ns | 0.94x | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Pipeline F→M→T→R** | 1.2µs / 1.8µs | **1.49x** | 12.3KB / 20.5KB | **1.66x less** | 4 / 4 | -| **Reduce (sum)** | 811ns / 780ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Reverse** | 748ns / 743ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Shuffle** | 4.3µs / 5.5µs | **1.27x** | 16.4KB / 8.2KB | 0.50x more | 4 / 2 | -| **Skip** | 667ns / 1.4µs | **2.03x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | -| **SkipLast** | 665ns / 1.4µs | **2.04x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | -| **Sum** | 837ns / 791ns | 0.94x | 8.2KB / 8.2KB | ≈ | 3 / 2 | -| **Take** | 667ns / 668ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 2 | -| **ToMap** | 8.1µs / 8.5µs | **1.05x** | 45.2KB / 45.2KB | ≈ | 7 / 8 | -| **Union** | 18.3µs / 18.7µs | ≈ | 106.7KB / 106.7KB | ≈ | 15 / 14 | -| **Unique** | 7.0µs / 6.8µs | ≈ | 53.4KB / 53.4KB | ≈ | 8 / 8 | -| **UniqueBy** | 7.3µs / 7.0µs | ≈ | 53.4KB / 53.4KB | ≈ | 9 / 8 | -| **Zip** | 2.6µs / 4.4µs | **1.69x** | 32.8KB / 32.8KB | ≈ | 5 / 5 | -| **ZipWith** | 2.2µs / 4.1µs | **1.83x** | 24.6KB / 24.6KB | ≈ | 5 / 5 | +| **Max** | 836ns / 789ns | 0.94x | 8.2KB / 8.2KB | ≈ | 3 / 2 | +| **Min** | 839ns / 792ns | 0.94x | 8.2KB / 8.2KB | ≈ | 3 / 2 | +| **None** | 829ns / 792ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Pipeline F→M→T→R** | 1.3µs / 1.9µs | **1.48x** | 12.3KB / 20.5KB | **1.66x less** | 4 / 4 | +| **Reduce (sum)** | 823ns / 811ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Reverse** | 764ns / 761ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | +| **Shuffle** | 4.3µs / 5.5µs | **1.28x** | 16.4KB / 8.2KB | 0.50x more | 4 / 2 | +| **Skip** | 676ns / 1.4µs | **2.05x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | +| **SkipLast** | 677ns / 1.4µs | **2.03x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | +| **Sum** | 835ns / 809ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 2 | +| **Take** | 686ns / 680ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 2 | +| **ToMap** | 8.0µs / 8.4µs | **1.05x** | 45.2KB / 45.2KB | ≈ | 7 / 8 | +| **Union** | 18.3µs / 18.9µs | ≈ | 106.7KB / 106.7KB | ≈ | 15 / 14 | +| **Unique** | 7.0µs / 7.0µs | ≈ | 53.4KB / 53.4KB | ≈ | 8 / 8 | +| **UniqueBy** | 7.4µs / 7.0µs | ≈ | 53.4KB / 53.4KB | ≈ | 9 / 8 | +| **Zip** | 2.7µs / 4.4µs | **1.64x** | 32.8KB / 32.8KB | ≈ | 5 / 5 | +| **ZipWith** | 2.3µs / 4.1µs | **1.82x** | 24.6KB / 24.6KB | ≈ | 5 / 5 | ## How to read the benchmarks diff --git a/docs/bench/main.go b/docs/bench/main.go index 63d1734..3484b59 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -21,7 +21,7 @@ const ( hotPathIters = 10_000 - benchInner = 8 + benchInner = 16 ) type benchResult struct { From 20eb58fc9dfe627ec8b77325c6c8bf50710062a4 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 22:02:18 -0600 Subject: [PATCH 08/30] docs: benchmarks --- README.md | 124 ++++++++++++++++++++++----------------------- docs/bench/main.go | 111 ++++++++++++++++------------------------ 2 files changed, 106 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index c35c0aa..d764d47 100644 --- a/README.md +++ b/README.md @@ -95,76 +95,76 @@ That design choice doesn't matter much for some single operations. It matters a | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 249ns / 233ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 249ns / 250ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 145ns / 1.1µs | **7.31x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 249ns / 234ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.0µs / 8.1µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.2µs / 8.3µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 19.0µs / 43.3µs | **2.28x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | -| **Each** | 255ns / 234ns | 0.92x | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 641ns / 1.1µs | **1.66x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **All** | 247ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 249ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 136ns / 1.0µs | **7.64x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 249ns / 231ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.1µs / 7.9µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.4µs / 7.8µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 19.3µs / 44.0µs | **2.28x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 251ns / 235ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 644ns / 1.0µs | **1.58x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | | **First** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 249ns / 234ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | +| **FirstWhere** | 247ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | | **GroupBySlice** | 8.0µs / 8.4µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 249ns / 235ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 11.0µs / 10.8µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **IndexWhere** | 248ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.0µs / 10.7µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | | **Last** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 845ns / 809ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **Max** | 255ns / 234ns | 0.92x | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 255ns / 234ns | 0.92x | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 249ns / 233ns | 0.94x | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 725ns / 1.3µs | **1.77x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | -| **Reduce (sum)** | 254ns / 234ns | 0.92x | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 227ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 3.9µs / 5.3µs | **1.36x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 11ns / 704ns | **63.98x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 11ns / 700ns | **63.63x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 264ns / 234ns | 0.89x | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 22ns / 0ns | 0.02x | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.5µs / 7.8µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 17.1µs / 17.6µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | -| **Unique** | 6.4µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.7µs / 6.3µs | 0.95x | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Map** | 804ns / 784ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **Max** | 267ns / 263ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 263ns / 232ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 250ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 777ns / 1.3µs | **1.64x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | +| **Reduce (sum)** | 246ns / 230ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 228ns / 238ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 3.9µs / 5.3µs | **1.35x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 11ns / 712ns | **64.69x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 11ns / 708ns | **63.62x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 264ns / 233ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 22ns / 0ns | 0.01x | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 8.0µs / 7.9µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 16.9µs / 17.5µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Unique** | 6.3µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **UniqueBy** | 6.7µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | | **Zip** | 1.4µs / 3.1µs | **2.22x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.0µs / 2.9µs | **2.91x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **ZipWith** | 1.0µs / 3.0µs | **2.93x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | #### Safe-by-default (New) vs lo | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 830ns / 788ns | 0.95x | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Any** | 828ns / 787ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Chunk** | 794ns / 1.7µs | **2.18x** | 9.5KB / 17.5KB | **1.84x less** | 3 / 53 | -| **Contains** | 832ns / 795ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **CountBy** | 8.7µs / 8.7µs | ≈ | 17.6KB / 17.6KB | ≈ | 13 / 13 | -| **CountByValue** | 8.9µs / 8.7µs | ≈ | 17.6KB / 17.6KB | ≈ | 13 / 13 | -| **Difference** | 20.2µs / 44.8µs | **2.22x** | 98.5KB / 125.3KB | **1.27x less** | 16 / 47 | -| **Each** | 819ns / 794ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Filter** | 1.1µs / 1.6µs | **1.48x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | -| **First** | 670ns / 669ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **FirstWhere** | 832ns / 791ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **GroupBySlice** | 8.7µs / 9.1µs | ≈ | 29.2KB / 29.2KB | ≈ | 85 / 85 | -| **IndexWhere** | 827ns / 788ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Intersect** | 12.1µs / 11.9µs | ≈ | 27.8KB / 27.8KB | ≈ | 24 / 23 | -| **Last** | 670ns / 667ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Map** | 1.4µs / 1.4µs | ≈ | 16.4KB / 16.4KB | ≈ | 3 / 3 | -| **Max** | 836ns / 789ns | 0.94x | 8.2KB / 8.2KB | ≈ | 3 / 2 | -| **Min** | 839ns / 792ns | 0.94x | 8.2KB / 8.2KB | ≈ | 3 / 2 | -| **None** | 829ns / 792ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Pipeline F→M→T→R** | 1.3µs / 1.9µs | **1.48x** | 12.3KB / 20.5KB | **1.66x less** | 4 / 4 | -| **Reduce (sum)** | 823ns / 811ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Reverse** | 764ns / 761ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 2 | -| **Shuffle** | 4.3µs / 5.5µs | **1.28x** | 16.4KB / 8.2KB | 0.50x more | 4 / 2 | -| **Skip** | 676ns / 1.4µs | **2.05x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | -| **SkipLast** | 677ns / 1.4µs | **2.03x** | 8.2KB / 16.4KB | **2.00x less** | 2 / 3 | -| **Sum** | 835ns / 809ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 2 | -| **Take** | 686ns / 680ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 2 | -| **ToMap** | 8.0µs / 8.4µs | **1.05x** | 45.2KB / 45.2KB | ≈ | 7 / 8 | -| **Union** | 18.3µs / 18.9µs | ≈ | 106.7KB / 106.7KB | ≈ | 15 / 14 | -| **Unique** | 7.0µs / 7.0µs | ≈ | 53.4KB / 53.4KB | ≈ | 8 / 8 | -| **UniqueBy** | 7.4µs / 7.0µs | ≈ | 53.4KB / 53.4KB | ≈ | 9 / 8 | -| **Zip** | 2.7µs / 4.4µs | **1.64x** | 32.8KB / 32.8KB | ≈ | 5 / 5 | -| **ZipWith** | 2.3µs / 4.1µs | **1.82x** | 24.6KB / 24.6KB | ≈ | 5 / 5 | +| **All** | 850ns / 235ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Any** | 853ns / 234ns | 0.27x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Chunk** | 813ns / 1.1µs | **1.30x** | 9.5KB / 9.3KB | ≈ | 3 / 51 | +| **Contains** | 842ns / 234ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **CountBy** | 8.7µs / 8.0µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 13 / 11 | +| **CountByValue** | 9.0µs / 8.3µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 13 / 11 | +| **Difference** | 20.3µs / 43.5µs | **2.14x** | 98.5KB / 108.8KB | **1.10x less** | 16 / 43 | +| **Each** | 844ns / 236ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Filter** | 1.1µs / 1.0µs | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **First** | 684ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **FirstWhere** | 850ns / 233ns | 0.27x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **GroupBySlice** | 8.6µs / 8.6µs | ≈ | 29.2KB / 21.0KB | 0.72x more | 85 / 83 | +| **IndexWhere** | 845ns / 233ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Intersect** | 12.5µs / 10.8µs | 0.86x | 27.8KB / 11.4KB | 0.41x more | 24 / 19 | +| **Last** | 687ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Map** | 1.4µs / 794ns | 0.55x | 16.4KB / 8.2KB | 0.50x more | 3 / 1 | +| **Max** | 841ns / 235ns | 0.28x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Min** | 854ns / 230ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **None** | 852ns / 232ns | 0.27x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Pipeline F→M→T→R** | 1.3µs / 1.3µs | ≈ | 12.3KB / 12.3KB | ≈ | 4 / 2 | +| **Reduce (sum)** | 841ns / 230ns | 0.27x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Reverse** | 780ns / 240ns | 0.31x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Shuffle** | 4.3µs / 5.3µs | **1.22x** | 16.4KB / 0B | ∞x more | 4 / 0 | +| **Skip** | 675ns / 711ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **SkipLast** | 680ns / 714ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **Sum** | 845ns / 230ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Take** | 685ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **ToMap** | 8.0µs / 7.8µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 7 / 6 | +| **Union** | 18.1µs / 17.7µs | ≈ | 106.7KB / 90.3KB | 0.85x more | 15 / 10 | +| **Unique** | 7.0µs / 6.3µs | ≈ | 53.4KB / 45.1KB | 0.85x more | 8 / 6 | +| **UniqueBy** | 7.3µs / 6.4µs | 0.88x | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | +| **Zip** | 2.6µs / 3.2µs | **1.19x** | 32.8KB / 16.4KB | 0.50x more | 5 / 1 | +| **ZipWith** | 2.3µs / 2.9µs | **1.29x** | 24.6KB / 8.2KB | 0.33x more | 5 / 1 | ## How to read the benchmarks diff --git a/docs/bench/main.go b/docs/bench/main.go index 3484b59..bc36dd0 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -21,7 +21,7 @@ const ( hotPathIters = 10_000 - benchInner = 16 + benchInner = 8 ) type benchResult struct { @@ -69,7 +69,6 @@ var ( ctorInts func([]int) *collection.Collection[int] ctorNumericInt func([]int) *collection.NumericCollection[int] currentMode benchMode - loWrapperSink []byte ) func setBenchMode(mode benchMode) { @@ -247,7 +246,7 @@ func benchPipelineLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := loInputCopied(benchInts) + input := benchInts out := lo.Filter(input, func(v int, _ int) bool { return v%2 == 0 }) out2 := lo.Map(out, func(v int, _ int) int { return v * v }) @@ -272,7 +271,7 @@ func benchAllLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.EveryBy(loInput(benchInts), func(v int) bool { return v < benchSize+1 }) + _ = lo.EveryBy(benchInts, func(v int) bool { return v < benchSize+1 }) } } @@ -292,7 +291,7 @@ func benchAnyLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.SomeBy(loInput(benchInts), func(v int) bool { return v == benchSize-1 }) + _ = lo.SomeBy(benchInts, func(v int) bool { return v == benchSize-1 }) } } @@ -312,7 +311,7 @@ func benchNoneLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.NoneBy(loInput(benchInts), func(v int) bool { return v < 0 }) + _ = lo.NoneBy(benchInts, func(v int) bool { return v < 0 }) } } @@ -332,7 +331,7 @@ func benchFirstLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _, _ = lo.First(loInput(benchInts)) + _, _ = lo.First(benchInts) } } @@ -352,7 +351,7 @@ func benchLastLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _, _ = lo.Last(loInput(benchInts)) + _, _ = lo.Last(benchInts) } } @@ -372,7 +371,7 @@ func benchIndexWhereLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _, _, _ = lo.FindIndexOf(loInput(benchInts), func(v int) bool { return v == benchSize-1 }) + _, _, _ = lo.FindIndexOf(benchInts, func(v int) bool { return v == benchSize-1 }) } } @@ -394,7 +393,7 @@ func benchEachLo(b *testing.B) { for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { sum := 0 - lo.ForEach(loInput(benchInts), func(v int, _ int) { sum += v }) + lo.ForEach(benchInts, func(v int, _ int) { sum += v }) } } @@ -404,7 +403,7 @@ func benchMapCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := collectionInputCopied(benchInts) + input := benchInts _ = ctorInts(input).Map(func(v int) int { return v * 3 }) } @@ -415,7 +414,7 @@ func benchMapLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := loInputCopied(benchInts) + input := benchInts _ = lo.Map(input, func(v int, _ int) int { return v * 3 }) } @@ -436,7 +435,7 @@ func benchReduceLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Reduce(loInput(benchInts), func(acc int, v int, _ int) int { return acc + v }, 0) + _ = lo.Reduce(benchInts, func(acc int, v int, _ int) int { return acc + v }, 0) } } @@ -457,7 +456,7 @@ func benchFilterLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := loInputCopied(benchInts) + input := benchInts _ = lo.Filter(input, func(v int, _ int) bool { return v%3 == 0 }) } @@ -478,7 +477,7 @@ func benchChunkLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Chunk(loInput(benchInts), benchChunkSize) + _ = lo.Chunk(benchInts, benchChunkSize) } } @@ -498,7 +497,7 @@ func benchTakeLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Subset(loInput(benchInts), 0, uint(benchTakeN)) + _ = lo.Subset(benchInts, 0, uint(benchTakeN)) } } @@ -518,7 +517,7 @@ func benchContainsLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.ContainsBy(loInput(benchInts), func(v int) bool { return v == benchSize-1 }) + _ = lo.ContainsBy(benchInts, func(v int) bool { return v == benchSize-1 }) } } @@ -538,7 +537,7 @@ func benchFindLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _, _ = lo.Find(loInput(benchInts), func(v int) bool { return v == benchSize-1 }) + _, _ = lo.Find(benchInts, func(v int) bool { return v == benchSize-1 }) } } @@ -558,7 +557,7 @@ func benchGroupByLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.GroupBy(loInput(benchInts), func(v int) int { return v % benchGroupByMod }) + _ = lo.GroupBy(benchInts, func(v int) int { return v % benchGroupByMod }) } } @@ -578,7 +577,7 @@ func benchCountByLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.CountValuesBy(loInput(benchIntsDup), func(v int) int { return v }) + _ = lo.CountValuesBy(benchIntsDup, func(v int) int { return v }) } } @@ -598,7 +597,7 @@ func benchCountByValueLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.CountValues(loInput(benchIntsDup)) + _ = lo.CountValues(benchIntsDup) } } @@ -618,7 +617,7 @@ func benchSkipLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Drop(loInput(benchInts), benchSkipN) + _ = lo.Drop(benchInts, benchSkipN) } } @@ -638,7 +637,7 @@ func benchSkipLastLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.DropRight(loInput(benchInts), benchSkipN) + _ = lo.DropRight(benchInts, benchSkipN) } } @@ -659,8 +658,8 @@ func benchReverseLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := loInputCopied(benchInts) - _ = lo.Reverse(input) + copy(workB, benchInts) + _ = lo.Reverse(workB) } } @@ -681,8 +680,8 @@ func benchShuffleLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := loInputCopied(benchInts) - _ = lo.Shuffle(input) + copy(workB, benchInts) + _ = lo.Shuffle(workB) } } @@ -702,7 +701,7 @@ func benchZipLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Zip2(loInput(benchInts), loInput(benchIntsDup)) + _ = lo.Zip2(benchInts, benchIntsDup) } } @@ -724,7 +723,7 @@ func benchZipWithLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.ZipBy2(loInput(benchInts), loInput(benchIntsDup), func(a, b int) int { + _ = lo.ZipBy2(benchInts, benchIntsDup, func(a, b int) int { return a + b }) @@ -746,7 +745,7 @@ func benchUniqueLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Uniq(loInput(benchIntsDup)) + _ = lo.Uniq(benchIntsDup) } } @@ -766,7 +765,7 @@ func benchUniqueByLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.UniqBy(loInput(benchIntsDup), func(v int) int { return v }) + _ = lo.UniqBy(benchIntsDup, func(v int) int { return v }) } } @@ -786,7 +785,7 @@ func benchUnionLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Union(loInput(unionLeft), loInput(unionRight)) + _ = lo.Union(unionLeft, unionRight) } } @@ -806,7 +805,7 @@ func benchIntersectLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Intersect(loInput(intersectLeft), loInput(intersectRight)) + _ = lo.Intersect(intersectLeft, intersectRight) } } @@ -826,7 +825,7 @@ func benchDifferenceLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _, _ = lo.Difference(loInput(differenceLeft), loInput(differenceRight)) + _, _ = lo.Difference(differenceLeft, differenceRight) } } @@ -846,7 +845,7 @@ func benchToMapLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.SliceToMap(loInput(benchInts), func(v int) (int, int) { return v, v }) + _ = lo.SliceToMap(benchInts, func(v int) (int, int) { return v, v }) } } @@ -866,7 +865,7 @@ func benchSumLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Sum(loInput(benchInts)) + _ = lo.Sum(benchInts) } } @@ -886,7 +885,7 @@ func benchMinLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Min(loInput(benchInts)) + _ = lo.Min(benchInts) } } @@ -906,7 +905,7 @@ func benchMaxLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - _ = lo.Max(loInput(benchInts)) + _ = lo.Max(benchInts) } } @@ -1004,14 +1003,20 @@ func formatDurationNs(ns float64) string { } } +const wrapperEpsilon = 0.10 // ±10% wrapper overhead tolerance + func formatRatio(lo, col float64) string { if col == 0 { return "∞" } + ratio := lo / col - if ratio >= 0.95 && ratio <= 1.05 { + + // Treat small deltas as equivalent (wrapper overhead, measurement noise) + if ratio >= 1-wrapperEpsilon && ratio <= 1+wrapperEpsilon { return "≈" } + out := fmt.Sprintf("%.2fx", ratio) if ratio > 1 { return fmt.Sprintf("**%s**", out) @@ -1063,34 +1068,6 @@ func collectionInputCopied(src []int) []int { return src } -func loInputCopied(src []int) []int { - if currentMode == benchNew { - loWrapAlloc() - cp := make([]int, len(src)) - copy(cp, src) - return cp - } - copy(workB, src) - return workB -} - -func loInput(src []int) []int { - if currentMode == benchNew { - loWrapAlloc() - cp := make([]int, len(src)) - copy(cp, src) - return cp - } - return src -} - -func loWrapAlloc() { - if currentMode != benchNew { - return - } - loWrapperSink = make([]byte, 24) -} - // ---------------------------------------------------------------------------- // README injection // ---------------------------------------------------------------------------- From 2edf6045ac1837cbd4a1ff969be1299a75d14d73 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 23:15:56 -0600 Subject: [PATCH 09/30] chore: semantics --- README.md | 202 +++++++++--------- after.go | 4 +- append.go | 2 +- before.go | 2 +- clone.go | 2 +- collection.go | 70 +++--- constructor_test.go | 41 ++-- difference.go | 4 +- docs/bench/main.go | 50 ++--- examples/{attach => copyof}/main.go | 8 +- .../{attachnumeric => copyofnumeric}/main.go | 8 +- from_map.go | 2 +- intersect.go | 4 +- map.go | 2 +- merge.go | 4 +- multiply.go | 4 +- partition.go | 4 +- pluck.go | 2 +- reverse_test.go | 2 +- shuffle.go | 2 +- skip.go | 6 +- skip_last.go | 6 +- skip_last_test.go | 2 +- skip_test.go | 2 +- symmetric_difference.go | 2 +- take.go | 10 +- take_last.go | 6 +- take_last_test.go | 2 +- take_until.go | 4 +- times.go | 4 +- union.go | 2 +- unique.go | 2 +- unique_by.go | 4 +- unique_comparable.go | 4 +- window.go | 6 +- zip.go | 4 +- 36 files changed, 245 insertions(+), 240 deletions(-) rename examples/{attach => copyof}/main.go (60%) rename examples/{attachnumeric => copyofnumeric}/main.go (57%) diff --git a/README.md b/README.md index d764d47..2b40b12 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Latest tag - Tests + Tests Go Report Card

@@ -91,80 +91,80 @@ That design choice doesn't matter much for some single operations. It matters a -#### Operator-only (Attach) vs lo +#### Default (New, borrowed) vs lo | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 247ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 249ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 136ns / 1.0µs | **7.64x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 249ns / 231ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.1µs / 7.9µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.4µs / 7.8µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 19.3µs / 44.0µs | **2.28x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | -| **Each** | 251ns / 235ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 644ns / 1.0µs | **1.58x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **All** | 249ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 249ns / 234ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 138ns / 1.0µs | **7.55x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 7.9µs / 7.9µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.2µs / 8.5µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 18.7µs / 42.9µs | **2.29x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 248ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 626ns / 1.0µs | **1.64x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | | **First** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 247ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 8.0µs / 8.4µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 248ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 11.0µs / 10.7µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **FirstWhere** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 7.9µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 10.8µs / 10.5µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | | **Last** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 804ns / 784ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **Max** | 267ns / 263ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 263ns / 232ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 250ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 777ns / 1.3µs | **1.64x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | -| **Reduce (sum)** | 246ns / 230ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 228ns / 238ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 3.9µs / 5.3µs | **1.35x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 11ns / 712ns | **64.69x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 11ns / 708ns | **63.62x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 264ns / 233ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 22ns / 0ns | 0.01x | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 8.0µs / 7.9µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 16.9µs / 17.5µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Map** | 829ns / 788ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **Max** | 265ns / 249ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 264ns / 232ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 767ns / 1.2µs | **1.62x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | +| **Reduce (sum)** | 248ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 225ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 3.8µs / 5.2µs | **1.37x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 11ns / 709ns | **65.92x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 11ns / 708ns | **65.15x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 264ns / 232ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 21ns / 0ns | 0.01x | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.5µs / 7.8µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 17.1µs / 17.5µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | | **Unique** | 6.3µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.7µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.4µs / 3.1µs | **2.22x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.0µs / 3.0µs | **2.93x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **UniqueBy** | 6.6µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.4µs / 3.1µs | **2.21x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.0µs / 2.9µs | **2.85x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | -#### Safe-by-default (New) vs lo +#### Explicit Copy (CopyOf) vs lo | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 850ns / 235ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Any** | 853ns / 234ns | 0.27x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Chunk** | 813ns / 1.1µs | **1.30x** | 9.5KB / 9.3KB | ≈ | 3 / 51 | -| **Contains** | 842ns / 234ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **CountBy** | 8.7µs / 8.0µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 13 / 11 | -| **CountByValue** | 9.0µs / 8.3µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 13 / 11 | +| **All** | 840ns / 233ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Any** | 1.0µs / 236ns | 0.24x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Chunk** | 799ns / 1.1µs | **1.34x** | 9.5KB / 9.3KB | ≈ | 3 / 51 | +| **Contains** | 833ns / 234ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **CountBy** | 8.4µs / 7.9µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 13 / 11 | +| **CountByValue** | 8.7µs / 8.5µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 13 / 11 | | **Difference** | 20.3µs / 43.5µs | **2.14x** | 98.5KB / 108.8KB | **1.10x less** | 16 / 43 | -| **Each** | 844ns / 236ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Each** | 831ns / 232ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | | **Filter** | 1.1µs / 1.0µs | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **First** | 684ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **FirstWhere** | 850ns / 233ns | 0.27x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **GroupBySlice** | 8.6µs / 8.6µs | ≈ | 29.2KB / 21.0KB | 0.72x more | 85 / 83 | -| **IndexWhere** | 845ns / 233ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Intersect** | 12.5µs / 10.8µs | 0.86x | 27.8KB / 11.4KB | 0.41x more | 24 / 19 | -| **Last** | 687ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Map** | 1.4µs / 794ns | 0.55x | 16.4KB / 8.2KB | 0.50x more | 3 / 1 | -| **Max** | 841ns / 235ns | 0.28x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Min** | 854ns / 230ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **None** | 852ns / 232ns | 0.27x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **First** | 677ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **FirstWhere** | 842ns / 234ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **GroupBySlice** | 8.5µs / 8.3µs | ≈ | 29.2KB / 21.0KB | 0.72x more | 85 / 83 | +| **IndexWhere** | 855ns / 234ns | 0.27x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Intersect** | 11.8µs / 10.7µs | ≈ | 27.8KB / 11.4KB | 0.41x more | 24 / 19 | +| **Last** | 688ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Map** | 1.4µs / 799ns | 0.55x | 16.4KB / 8.2KB | 0.50x more | 3 / 1 | +| **Max** | 848ns / 232ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Min** | 846ns / 232ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **None** | 911ns / 234ns | 0.26x | 8.2KB / 0B | ∞x more | 2 / 0 | | **Pipeline F→M→T→R** | 1.3µs / 1.3µs | ≈ | 12.3KB / 12.3KB | ≈ | 4 / 2 | -| **Reduce (sum)** | 841ns / 230ns | 0.27x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Reverse** | 780ns / 240ns | 0.31x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Shuffle** | 4.3µs / 5.3µs | **1.22x** | 16.4KB / 0B | ∞x more | 4 / 0 | -| **Skip** | 675ns / 711ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **SkipLast** | 680ns / 714ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **Sum** | 845ns / 230ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Take** | 685ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **ToMap** | 8.0µs / 7.8µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 7 / 6 | -| **Union** | 18.1µs / 17.7µs | ≈ | 106.7KB / 90.3KB | 0.85x more | 15 / 10 | -| **Unique** | 7.0µs / 6.3µs | ≈ | 53.4KB / 45.1KB | 0.85x more | 8 / 6 | -| **UniqueBy** | 7.3µs / 6.4µs | 0.88x | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | -| **Zip** | 2.6µs / 3.2µs | **1.19x** | 32.8KB / 16.4KB | 0.50x more | 5 / 1 | -| **ZipWith** | 2.3µs / 2.9µs | **1.29x** | 24.6KB / 8.2KB | 0.33x more | 5 / 1 | +| **Reduce (sum)** | 838ns / 240ns | 0.29x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Reverse** | 780ns / 233ns | 0.30x | 8.2KB / 0B | ∞x more | 2 / 0 | +| **Shuffle** | 4.2µs / 5.2µs | **1.24x** | 16.4KB / 0B | ∞x more | 4 / 0 | +| **Skip** | 680ns / 714ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **SkipLast** | 677ns / 715ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **Sum** | 847ns / 233ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Take** | 693ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **ToMap** | 8.1µs / 7.8µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 7 / 6 | +| **Union** | 18.1µs / 17.5µs | ≈ | 106.7KB / 90.3KB | 0.85x more | 15 / 10 | +| **Unique** | 7.0µs / 6.4µs | ≈ | 53.4KB / 45.1KB | 0.85x more | 8 / 6 | +| **UniqueBy** | 7.3µs / 6.3µs | 0.87x | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | +| **Zip** | 2.6µs / 3.1µs | **1.19x** | 32.8KB / 16.4KB | 0.50x more | 5 / 1 | +| **ZipWith** | 2.3µs / 2.9µs | **1.27x** | 24.6KB / 8.2KB | 0.33x more | 5 / 1 | ## How to read the benchmarks @@ -216,6 +216,8 @@ In these cases, `collection` can be **2×–30× faster** and often reduce alloc Fluent pipelines don't mean you're locked into mutation. +`New` borrows the input slice by default. If you need an owned copy, use `CopyOf()` or `Clone()` explicitly. + 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. ```go @@ -299,7 +301,7 @@ go get github.com/goforj/collection |------:|-----------| | **Access** | [Items](#items) [ItemsCopy](#itemscopy) | | **Aggregation** | [Avg](#avg) [Count](#count) [CountBy](#countby) [CountByValue](#countbyvalue) [Max](#max) [MaxBy](#maxby) [Median](#median) [Min](#min) [MinBy](#minby) [Mode](#mode) [Reduce](#reduce) [Sum](#sum) | -| **Construction** | [Attach](#attach) [AttachNumeric](#attachnumeric) [Clone](#clone) [New](#new) [NewNumeric](#newnumeric) | +| **Construction** | [Clone](#clone) [CopyOf](#copyof) [CopyOfNumeric](#copyofnumeric) [New](#new) [NewNumeric](#newnumeric) | | **Debugging** | [Dd](#dd) [Dump](#dump) [DumpStr](#dumpstr) | | **Grouping** | [GroupBy](#groupby) [GroupBySlice](#groupbyslice) | | **Maps** | [FromMap](#frommap) [ToMap](#tomap) [ToMapKV](#tomapkv) | @@ -898,40 +900,6 @@ collection.Dump(total3) ## Construction -### Attach · immutable · fluent - -Attach wraps a slice without copying. - -```go -items := []int{1, 2, 3} -c := collection.Attach(items) - -items[0] = 9 -collection.Dump(c.Items()) -// #[]int [ -// 0 => 9 #int -// 1 => 2 #int -// 2 => 3 #int -// ] -``` - -### AttachNumeric · immutable · fluent - -AttachNumeric wraps a slice of numeric types without copying. - -```go -items := []int{1, 2, 3} -c := collection.AttachNumeric(items) - -items[0] = 9 -collection.Dump(c.Items()) -// #[]int [ -// 0 => 9 #int -// 1 => 2 #int -// 2 => 3 #int -// ] -``` - ### Clone · immutable · fluent Clone returns a copy of the collection. @@ -996,13 +964,47 @@ collection.Dump(odds.Items()) // ] ``` +### CopyOf · immutable · fluent + +CopyOf creates a new Collection by copying the provided slice. + +```go +items := []int{1, 2, 3} +c := collection.CopyOf(items) + +items[0] = 9 +collection.Dump(c.Items()) +// #[]int [ +// 0 => 1 #int +// 1 => 2 #int +// 2 => 3 #int +// ] +``` + +### CopyOfNumeric · immutable · fluent + +CopyOfNumeric creates a new NumericCollection by copying the provided slice. + +```go +items := []int{1, 2, 3} +c := collection.CopyOfNumeric(items) + +items[0] = 9 +collection.Dump(c.Items()) +// #[]int [ +// 0 => 1 #int +// 1 => 2 #int +// 2 => 3 #int +// ] +``` + ### New · immutable · fluent -New creates a new Collection from the provided slice. +New creates a new Collection from the provided slice and borrows it. ### NewNumeric · immutable · fluent -NewNumeric wraps a slice of numeric types in a NumericCollection. +NewNumeric wraps a slice of numeric types in a NumericCollection and borrows it. ## Debugging diff --git a/after.go b/after.go index 7e64a90..a5aba36 100644 --- a/after.go +++ b/after.go @@ -27,8 +27,8 @@ func (c *Collection[T]) After(pred func(T) bool) *Collection[T] { // If no match found → empty collection if idx == -1 || idx+1 >= len(c.items) { - return Attach(c.items[:0]) + return New(c.items[:0]) } - return Attach(c.items[idx+1:]) + return New(c.items[idx+1:]) } diff --git a/append.go b/append.go index 6efbf43..957e63e 100644 --- a/append.go +++ b/append.go @@ -54,5 +54,5 @@ func (c *Collection[T]) Append(values ...T) *Collection[T] { out := make([]T, 0, len(c.items)+len(values)) out = append(out, c.items...) out = append(out, values...) - return Attach(out) + return New(out) } diff --git a/before.go b/before.go index aa691cc..9d44d32 100644 --- a/before.go +++ b/before.go @@ -60,5 +60,5 @@ func (c *Collection[T]) Before(pred func(T) bool) *Collection[T] { } } - return Attach(c.items[:idx]) + return New(c.items[:idx]) } diff --git a/clone.go b/clone.go index 06f938e..69cea1b 100644 --- a/clone.go +++ b/clone.go @@ -70,5 +70,5 @@ package collection func (c *Collection[T]) Clone() *Collection[T] { out := make([]T, len(c.items)) copy(out, c.items) - return Attach(out) + return &Collection[T]{items: out} } diff --git a/collection.go b/collection.go index f7224c5..fa78541 100644 --- a/collection.go +++ b/collection.go @@ -21,7 +21,7 @@ type Pair[K comparable, V any] struct { Value V } -// New creates a new Collection from the provided slice. +// New creates a new Collection from the provided slice and borrows it. // @group Construction // @behavior immutable // @fluent true @@ -29,17 +29,8 @@ type Pair[K comparable, V any] struct { // The returned Collection is a lightweight, strongly-typed wrapper // around the slice, enabling fluent, chainable operations such as // filtering, mapping, reducing, sorting, and more. -// -// New copies the input slice to avoid shared backing. func New[T any](items []T) *Collection[T] { - var out []T - if items == nil { - out = nil - } else { - out = make([]T, len(items)) - copy(out, items) - } - return &Collection[T]{items: out} + return &Collection[T]{items: items} } // NumericCollection is a Collection specialized for numeric types. @@ -47,71 +38,74 @@ type NumericCollection[T Number] struct { *Collection[T] } -// NewNumeric wraps a slice of numeric types in a NumericCollection. +// NewNumeric wraps a slice of numeric types in a NumericCollection and borrows it. // @group Construction // @behavior immutable // @fluent true -// -// NewNumeric copies the input slice to avoid shared backing. func NewNumeric[T Number](items []T) *NumericCollection[T] { - var out []T - if items == nil { - out = nil - } else { - out = make([]T, len(items)) - copy(out, items) - } return &NumericCollection[T]{ - Collection: &Collection[T]{items: out}, + Collection: &Collection[T]{items: items}, } } -// Attach wraps a slice without copying. +// CopyOf creates a new Collection by copying the provided slice. // @group Construction // @behavior immutable // @fluent true // -// Attach shares the backing array with the caller. Mutating either side -// will affect the other. Use New to copy the input. +// CopyOf allocates a new backing slice. // -// Example: sharing backing slice +// Example: copying input slice // // items := []int{1, 2, 3} -// c := collection.Attach(items) +// c := collection.CopyOf(items) // // items[0] = 9 // collection.Dump(c.Items()) // // #[]int [ -// // 0 => 9 #int +// // 0 => 1 #int // // 1 => 2 #int // // 2 => 3 #int // // ] -func Attach[T any](items []T) *Collection[T] { - return &Collection[T]{items: items} +func CopyOf[T any](items []T) *Collection[T] { + var out []T + if items == nil { + out = nil + } else { + out = make([]T, len(items)) + copy(out, items) + } + return &Collection[T]{items: out} } -// AttachNumeric wraps a slice of numeric types without copying. +// CopyOfNumeric creates a new NumericCollection by copying the provided slice. // @group Construction // @behavior immutable // @fluent true // -// AttachNumeric shares the backing array with the caller. Mutating either side -// will affect the other. Use NewNumeric to copy the input. +// CopyOfNumeric allocates a new backing slice. // -// Example: sharing backing slice +// Example: copying input slice // // items := []int{1, 2, 3} -// c := collection.AttachNumeric(items) +// c := collection.CopyOfNumeric(items) // // items[0] = 9 // collection.Dump(c.Items()) // // #[]int [ -// // 0 => 9 #int +// // 0 => 1 #int // // 1 => 2 #int // // 2 => 3 #int // // ] -func AttachNumeric[T Number](items []T) *NumericCollection[T] { - return &NumericCollection[T]{Collection: &Collection[T]{items: items}} +func CopyOfNumeric[T Number](items []T) *NumericCollection[T] { + var out []T + if items == nil { + out = nil + } else { + out = make([]T, len(items)) + copy(out, items) + } + return &NumericCollection[T]{Collection: &Collection[T]{items: out}} } // Items returns the backing slice of items. diff --git a/constructor_test.go b/constructor_test.go index 076a1f6..8fe6fb4 100644 --- a/constructor_test.go +++ b/constructor_test.go @@ -2,14 +2,14 @@ package collection import "testing" -func TestNew_CopiesInputSlice(t *testing.T) { +func TestNew_BorrowsInputSlice(t *testing.T) { items := []int{1, 2, 3} c := New(items) items[0] = 9 - if c.Items()[0] == 9 { - t.Fatalf("New should copy input slice") + if c.Items()[0] != 9 { + t.Fatalf("New should borrow input slice") } } @@ -22,14 +22,23 @@ func TestNew_PreservesNilSlice(t *testing.T) { } } -func TestAttach_SharesBackingSlice(t *testing.T) { +func TestCopyOf_CopiesInputSlice(t *testing.T) { items := []int{1, 2, 3} - c := Attach(items) + c := CopyOf(items) items[0] = 9 - if c.Items()[0] != 9 { - t.Fatalf("Attach should share backing slice") + if c.Items()[0] == 9 { + t.Fatalf("CopyOf should copy input slice") + } +} + +func TestCopyOf_PreservesNilSlice(t *testing.T) { + var items []int + c := CopyOf(items) + + if c.Items() != nil { + t.Fatalf("CopyOf should preserve nil slice") } } @@ -42,31 +51,31 @@ func TestNewNumeric_PreservesNilSlice(t *testing.T) { } } -func TestNewNumeric_CopiesInputSlice(t *testing.T) { +func TestNewNumeric_BorrowsInputSlice(t *testing.T) { items := []int{1, 2, 3} c := NewNumeric(items) items[0] = 9 - if c.Items()[0] == 9 { - t.Fatalf("NewNumeric should copy input slice") + if c.Items()[0] != 9 { + t.Fatalf("NewNumeric should borrow input slice") } } -func TestAttachNumeric_SharesBackingSlice(t *testing.T) { +func TestCopyOfNumeric_CopiesInputSlice(t *testing.T) { items := []int{1, 2, 3} - c := AttachNumeric(items) + c := CopyOfNumeric(items) items[0] = 9 - if c.Items()[0] != 9 { - t.Fatalf("AttachNumeric should share backing slice") + if c.Items()[0] == 9 { + t.Fatalf("CopyOfNumeric should copy input slice") } } func TestSelectionOps_ShareBackingSlice(t *testing.T) { items := []int{1, 2, 3, 4} - c := Attach(items) + c := New(items) view := c.Take(2) items[0] = 9 @@ -78,7 +87,7 @@ func TestSelectionOps_ShareBackingSlice(t *testing.T) { func TestItemsCopy_ReturnsCopy(t *testing.T) { items := []int{1, 2, 3} - c := Attach(items) + c := New(items) copyItems := c.ItemsCopy() copyItems[0] = 9 diff --git a/difference.go b/difference.go index ab45c1a..9815e67 100644 --- a/difference.go +++ b/difference.go @@ -62,7 +62,7 @@ package collection // // ] func Difference[T comparable](a, b *Collection[T]) *Collection[T] { if len(a.items) == 0 { - return Attach([]T{}) + return New([]T{}) } lookup := make(map[T]struct{}, len(b.items)) @@ -84,5 +84,5 @@ func Difference[T comparable](a, b *Collection[T]) *Collection[T] { out = append(out, v) } - return Attach(out) + return New(out) } diff --git a/docs/bench/main.go b/docs/bench/main.go index bc36dd0..a690d84 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -38,12 +38,12 @@ func main() { start := time.Now() only := parseOnly(*onlyFlag) - attachResults := runBenches(only, benchAttach) - newResults := runBenches(only, benchNew) - attachTable := renderTable(attachResults) - newTable := renderTable(newResults) + borrowResults := runBenches(only, benchBorrow) + copyResults := runBenches(only, benchCopy) + borrowTable := renderTable(borrowResults) + copyTable := renderTable(copyResults) - if err := updateReadme(attachTable, newTable); err != nil { + if err := updateReadme(borrowTable, copyTable); err != nil { fmt.Println("Error:", err) os.Exit(1) } @@ -61,8 +61,8 @@ func main() { type benchMode string const ( - benchAttach benchMode = "attach" - benchNew benchMode = "new" + benchBorrow benchMode = "borrow" + benchCopy benchMode = "copy" ) var ( @@ -74,12 +74,12 @@ var ( func setBenchMode(mode benchMode) { currentMode = mode switch mode { - case benchNew: + case benchCopy: + ctorInts = collection.CopyOf[int] + ctorNumericInt = collection.CopyOfNumeric[int] + default: ctorInts = collection.New[int] ctorNumericInt = collection.NewNumeric[int] - default: - ctorInts = collection.Attach[int] - ctorNumericInt = collection.AttachNumeric[int] } } @@ -231,7 +231,7 @@ func benchPipelineCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := collectionInputCopied(benchInts) + input := collectionInputForMutating(benchInts) _ = ctorInts(input). Filter(func(v int) bool { return v%2 == 0 }). Map(func(v int) int { return v * v }). @@ -445,7 +445,7 @@ func benchFilterCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := collectionInputCopied(benchInts) + input := collectionInputForMutating(benchInts) _ = ctorInts(input).Filter(func(v int) bool { return v%3 == 0 }) } @@ -647,7 +647,7 @@ func benchReverseCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := collectionInputCopied(benchInts) + input := collectionInputForMutating(benchInts) _ = ctorInts(input).Reverse() } @@ -669,7 +669,7 @@ func benchShuffleCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := collectionInputCopied(benchInts) + input := collectionInputForMutating(benchInts) _ = ctorInts(input).Shuffle() } @@ -1035,7 +1035,7 @@ func formatRatioBytes(lo, col int64) string { } ratio := float64(lo) / float64(col) - if ratio >= 0.95 && ratio <= 1.05 { + if ratio >= 0.90 && ratio <= 1.10 { return "≈" } @@ -1060,8 +1060,8 @@ func formatInt(v int64) string { } } -func collectionInputCopied(src []int) []int { - if currentMode == benchAttach { +func collectionInputForMutating(src []int) []int { + if currentMode == benchBorrow { copy(workA, src) return workA } @@ -1072,7 +1072,7 @@ func collectionInputCopied(src []int) []int { // README injection // ---------------------------------------------------------------------------- -func updateReadme(attachTable, newTable string) error { +func updateReadme(borrowTable, copyTable string) error { root, err := findRoot() if err != nil { return err @@ -1084,7 +1084,7 @@ func updateReadme(attachTable, newTable string) error { return err } - out, err := replaceSection(string(data), attachTable, newTable) + out, err := replaceSection(string(data), borrowTable, copyTable) if err != nil { return err } @@ -1092,7 +1092,7 @@ func updateReadme(attachTable, newTable string) error { return os.WriteFile(readmePath, []byte(out), 0o644) } -func replaceSection(readme, attachTable, newTable string) (string, error) { +func replaceSection(readme, borrowTable, copyTable string) (string, error) { start := strings.Index(readme, benchStart) end := strings.Index(readme, benchEnd) if start == -1 || end == -1 || end < start { @@ -1100,7 +1100,7 @@ func replaceSection(readme, attachTable, newTable string) (string, error) { } section := readme[start+len(benchStart) : end] - updated, err := replaceBenchTables(section, attachTable, newTable) + updated, err := replaceBenchTables(section, borrowTable, copyTable) if err != nil { return "", err } @@ -1112,7 +1112,7 @@ func replaceSection(readme, attachTable, newTable string) (string, error) { return buf.String(), nil } -func replaceBenchTables(section, attachTable, newTable string) (string, error) { +func replaceBenchTables(section, borrowTable, copyTable string) (string, error) { lines := strings.Split(section, "\n") tableStarts := []int{} for i, line := range lines { @@ -1145,13 +1145,13 @@ func replaceBenchTables(section, attachTable, newTable string) (string, error) { buf.WriteString(strings.Join(lines[:firstStart], "\n")) buf.WriteString("\n") } - buf.WriteString(strings.TrimSpace(attachTable)) + buf.WriteString(strings.TrimSpace(borrowTable)) buf.WriteString("\n") buf.WriteString(strings.Join(lines[firstEnd:secondStart], "\n")) if buf.Len() > 0 && !strings.HasSuffix(buf.String(), "\n") { buf.WriteString("\n") } - buf.WriteString(strings.TrimSpace(newTable)) + buf.WriteString(strings.TrimSpace(copyTable)) buf.WriteString("\n") buf.WriteString(strings.Join(lines[secondEnd:], "\n")) return buf.String(), nil diff --git a/examples/attach/main.go b/examples/copyof/main.go similarity index 60% rename from examples/attach/main.go rename to examples/copyof/main.go index abd5f6b..8f95e8c 100644 --- a/examples/attach/main.go +++ b/examples/copyof/main.go @@ -6,16 +6,16 @@ package main import "github.com/goforj/collection" func main() { - // Attach wraps a slice without copying. + // CopyOf creates a new Collection by copying the provided slice. - // Example: sharing backing slice + // Example: copying input slice items := []int{1, 2, 3} - c := collection.Attach(items) + c := collection.CopyOf(items) items[0] = 9 collection.Dump(c.Items()) // #[]int [ - // 0 => 9 #int + // 0 => 1 #int // 1 => 2 #int // 2 => 3 #int // ] diff --git a/examples/attachnumeric/main.go b/examples/copyofnumeric/main.go similarity index 57% rename from examples/attachnumeric/main.go rename to examples/copyofnumeric/main.go index 5515675..8d0e615 100644 --- a/examples/attachnumeric/main.go +++ b/examples/copyofnumeric/main.go @@ -6,16 +6,16 @@ package main import "github.com/goforj/collection" func main() { - // AttachNumeric wraps a slice of numeric types without copying. + // CopyOfNumeric creates a new NumericCollection by copying the provided slice. - // Example: sharing backing slice + // Example: copying input slice items := []int{1, 2, 3} - c := collection.AttachNumeric(items) + c := collection.CopyOfNumeric(items) items[0] = 9 collection.Dump(c.Items()) // #[]int [ - // 0 => 9 #int + // 0 => 1 #int // 1 => 2 #int // 2 => 3 #int // ] diff --git a/from_map.go b/from_map.go index 22f75aa..d99b934 100644 --- a/from_map.go +++ b/from_map.go @@ -78,5 +78,5 @@ func FromMap[K comparable, V any](m map[K]V) *Collection[Pair[K, V]] { Value: v, }) } - return Attach(items) + return New(items) } diff --git a/intersect.go b/intersect.go index 74157f1..c898cef 100644 --- a/intersect.go +++ b/intersect.go @@ -68,7 +68,7 @@ package collection // // ] func Intersect[T comparable](a, b *Collection[T]) *Collection[T] { if len(a.items) == 0 || len(b.items) == 0 { - return Attach([]T{}) + return New([]T{}) } seen := make(map[T]struct{}) @@ -83,5 +83,5 @@ func Intersect[T comparable](a, b *Collection[T]) *Collection[T] { } } - return Attach(out) + return New(out) } diff --git a/map.go b/map.go index 3683309..0ca1c16 100644 --- a/map.go +++ b/map.go @@ -70,5 +70,5 @@ func (c *Collection[T]) Map(fn func(T) T) *Collection[T] { for i, v := range c.items { out[i] = fn(v) } - return Attach(out) + return New(out) } diff --git a/merge.go b/merge.go index 386e0ea..33d71fe 100644 --- a/merge.go +++ b/merge.go @@ -97,7 +97,7 @@ func (c *Collection[T]) mergeSlice(values []T) *Collection[T] { out := make([]T, len(c.items)+len(values)) copy(out, c.items) copy(out[len(c.items):], values) - return Attach(out) + return New(out) } /* @@ -150,7 +150,7 @@ func (c *Collection[T]) mergeMap(values map[string]T) *Collection[T] { } // IMPORTANT: return without copying out again - return Attach(out) + return New(out) } // fastParseInt is much lighter than strconv.Atoi diff --git a/multiply.go b/multiply.go index 71cdde4..3a455f2 100644 --- a/multiply.go +++ b/multiply.go @@ -64,7 +64,7 @@ package collection // // ] func (c *Collection[T]) Multiply(n int) *Collection[T] { if n <= 0 { - return Attach([]T{}) + return New([]T{}) } orig := c.items @@ -74,5 +74,5 @@ func (c *Collection[T]) Multiply(n int) *Collection[T] { out = append(out, orig...) } - return Attach(out) + return New(out) } diff --git a/partition.go b/partition.go index 918eea2..4107edb 100644 --- a/partition.go +++ b/partition.go @@ -76,7 +76,7 @@ package collection // // ] func (c *Collection[T]) Partition(fn func(T) bool) (*Collection[T], *Collection[T]) { if len(c.items) == 0 { - return Attach([]T{}), Attach([]T{}) + return New([]T{}), New([]T{}) } // Pre-size to total length; each partition will shrink to its own length. @@ -91,5 +91,5 @@ func (c *Collection[T]) Partition(fn func(T) bool) (*Collection[T], *Collection[ } } - return Attach(left), Attach(right) + return New(left), New(right) } diff --git a/pluck.go b/pluck.go index effec25..79e0ce0 100644 --- a/pluck.go +++ b/pluck.go @@ -64,5 +64,5 @@ func MapTo[T any, R any](c *Collection[T], fn func(T) R) *Collection[R] { for i, v := range items { out[i] = fn(v) } - return Attach(out) + return New(out) } diff --git a/reverse_test.go b/reverse_test.go index 753503e..98eb50e 100644 --- a/reverse_test.go +++ b/reverse_test.go @@ -86,7 +86,7 @@ func TestReverse_Chainable(t *testing.T) { func TestReverse_MutatesInPlace(t *testing.T) { items := []int{1, 2, 3} - c := Attach(items) + c := New(items) c.Reverse() diff --git a/shuffle.go b/shuffle.go index 84085f3..e13391e 100644 --- a/shuffle.go +++ b/shuffle.go @@ -67,5 +67,5 @@ func (c *Collection[T]) Shuffle() *Collection[T] { out[i], out[j] = out[j], out[i] } - return Attach(out) + return New(out) } diff --git a/skip.go b/skip.go index 0c2509d..8802c05 100644 --- a/skip.go +++ b/skip.go @@ -69,12 +69,12 @@ func (c *Collection[T]) Skip(n int) *Collection[T] { l := len(items) if n <= 0 { - return Attach(items) + return New(items) } if n >= l { - return Attach(items[:0]) + return New(items[:0]) } - return Attach(items[n:]) + return New(items[n:]) } diff --git a/skip_last.go b/skip_last.go index 85527a2..8c4cbba 100644 --- a/skip_last.go +++ b/skip_last.go @@ -65,12 +65,12 @@ func (c *Collection[T]) SkipLast(n int) *Collection[T] { l := len(items) if n <= 0 { - return Attach(items) + return New(items) } if n >= l { - return Attach(items[:0]) + return New(items[:0]) } - return Attach(items[:l-n]) + return New(items[:l-n]) } diff --git a/skip_last_test.go b/skip_last_test.go index f641858..ce1d7c2 100644 --- a/skip_last_test.go +++ b/skip_last_test.go @@ -94,7 +94,7 @@ func TestSkipLast_DoesNotMutateOriginal(t *testing.T) { func TestSkipLast_ReusesBackingSlice(t *testing.T) { items := []int{1, 2, 3, 4} - c := Attach(items) + c := New(items) out := c.SkipLast(1) out.Items()[0] = 99 diff --git a/skip_test.go b/skip_test.go index 6c7b0f5..a669bb2 100644 --- a/skip_test.go +++ b/skip_test.go @@ -94,7 +94,7 @@ func TestSkip_DoesNotMutateOriginal(t *testing.T) { func TestSkip_ReusesBackingSlice(t *testing.T) { items := []int{1, 2, 3, 4} - c := Attach(items) + c := New(items) out := c.Skip(1) diff --git a/symmetric_difference.go b/symmetric_difference.go index 46f4cd5..3151e7e 100644 --- a/symmetric_difference.go +++ b/symmetric_difference.go @@ -98,5 +98,5 @@ func SymmetricDifference[T comparable](a, b *Collection[T]) *Collection[T] { out = append(out, v) } - return Attach(out) + return New(out) } diff --git a/take.go b/take.go index 76301d1..94159d5 100644 --- a/take.go +++ b/take.go @@ -56,24 +56,24 @@ func (c *Collection[T]) Take(n int) *Collection[T] { // Empty or zero → empty collection if n == 0 || length == 0 { - return Attach([]T{}) + return New([]T{}) } // Positive → take from start if n > 0 { if n >= length { // no need to allocate; just reuse original - return Attach(c.items) + return New(c.items) } - return Attach(c.items[:n]) + return New(c.items[:n]) } // Negative → take from end n = -n if n >= length { - return Attach(c.items) + return New(c.items) } start := length - n - return Attach(c.items[start:]) + return New(c.items[start:]) } diff --git a/take_last.go b/take_last.go index 713a779..ea507ad 100644 --- a/take_last.go +++ b/take_last.go @@ -62,12 +62,12 @@ func (c *Collection[T]) TakeLast(n int) *Collection[T] { l := len(items) if n <= 0 { - return Attach(items[:0]) + return New(items[:0]) } if n >= l { - return Attach(items) + return New(items) } - return Attach(items[l-n:]) + return New(items[l-n:]) } diff --git a/take_last_test.go b/take_last_test.go index 6fc7853..62a5bba 100644 --- a/take_last_test.go +++ b/take_last_test.go @@ -93,7 +93,7 @@ func TestTakeLast_DoesNotMutateOriginal(t *testing.T) { func TestTakeLast_ReusesBackingSlice(t *testing.T) { items := []int{1, 2, 3, 4} - c := Attach(items) + c := New(items) out := c.TakeLast(2) out.Items()[0] = 99 diff --git a/take_until.go b/take_until.go index f960de9..0181ddc 100644 --- a/take_until.go +++ b/take_until.go @@ -44,7 +44,7 @@ func (c *Collection[T]) TakeUntilFn(pred func(T) bool) *Collection[T] { } } - return Attach(c.items[:idx]) + return New(c.items[:idx]) } // TakeUntil returns items until the first element equals `value`. @@ -93,5 +93,5 @@ func TakeUntil[T comparable](c *Collection[T], value T) *Collection[T] { } } - return Attach(c.items[:idx]) + return New(c.items[:idx]) } diff --git a/times.go b/times.go index c5a59a4..e1f958b 100644 --- a/times.go +++ b/times.go @@ -65,7 +65,7 @@ package collection // // ] func Times[T any](count int, fn func(int) T) *Collection[T] { if count <= 0 { - return Attach([]T{}) + return New([]T{}) } out := make([]T, count) @@ -73,5 +73,5 @@ func Times[T any](count int, fn func(int) T) *Collection[T] { out[i-1] = fn(i) } - return Attach(out) + return New(out) } diff --git a/union.go b/union.go index dccc8ed..1877777 100644 --- a/union.go +++ b/union.go @@ -88,5 +88,5 @@ func Union[T comparable](a, b *Collection[T]) *Collection[T] { out = append(out, v) } - return Attach(out) + return New(out) } diff --git a/unique.go b/unique.go index 7194b46..ae535d7 100644 --- a/unique.go +++ b/unique.go @@ -72,5 +72,5 @@ func (c *Collection[T]) Unique(eq func(a, b T) bool) *Collection[T] { } } - return Attach(out) + return New(out) } diff --git a/unique_by.go b/unique_by.go index 1b1e742..134d3fc 100644 --- a/unique_by.go +++ b/unique_by.go @@ -58,7 +58,7 @@ func UniqueBy[T any, K comparable](c *Collection[T], keyFn func(T) K) *Collectio items := c.items n := len(items) if n == 0 { - return Attach([]T{}) + return New([]T{}) } seen := make(map[K]struct{}, n) @@ -73,5 +73,5 @@ func UniqueBy[T any, K comparable](c *Collection[T], keyFn func(T) K) *Collectio out = append(out, v) } - return Attach(out) + return New(out) } diff --git a/unique_comparable.go b/unique_comparable.go index 0f67dea..e7817a9 100644 --- a/unique_comparable.go +++ b/unique_comparable.go @@ -33,7 +33,7 @@ package collection func UniqueComparable[T comparable](c *Collection[T]) *Collection[T] { n := len(c.items) if n == 0 { - return Attach([]T{}) + return New([]T{}) } seen := make(map[T]struct{}, n) @@ -48,5 +48,5 @@ func UniqueComparable[T comparable](c *Collection[T]) *Collection[T] { out = append(out, v) } - return Attach(out) + return New(out) } diff --git a/window.go b/window.go index 5ca84eb..daa643a 100644 --- a/window.go +++ b/window.go @@ -98,7 +98,7 @@ package collection // // ] func Window[T any](c *Collection[T], size int, step int) *Collection[[]T] { if size <= 0 { - return Attach([][]T(nil)) + return New([][]T(nil)) } if step <= 0 { @@ -107,7 +107,7 @@ func Window[T any](c *Collection[T], size int, step int) *Collection[[]T] { n := len(c.items) if n < size { - return Attach([][]T(nil)) + return New([][]T(nil)) } // Compute number of windows. @@ -118,5 +118,5 @@ func Window[T any](c *Collection[T], size int, step int) *Collection[[]T] { out = append(out, c.items[i:i+size]) } - return Attach(out) + return New(out) } diff --git a/zip.go b/zip.go index 0ce416f..d81253d 100644 --- a/zip.go +++ b/zip.go @@ -67,7 +67,7 @@ func Zip[A any, B any](a *Collection[A], b *Collection[B]) *Collection[Tuple[A, out[i] = Tuple[A, B]{First: a.items[i], Second: b.items[i]} } - return Attach(out) + return New(out) } // ZipWith combines two collections element-wise using combiner fn. @@ -138,5 +138,5 @@ func ZipWith[A any, B any, R any](a *Collection[A], b *Collection[B], fn func(A, out[i] = fn(a.items[i], b.items[i]) } - return Attach(out) + return New(out) } From f20eff3f83c6263cd42b925279e1e6022874f938 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 23:21:56 -0600 Subject: [PATCH 10/30] docs: update --- README.md | 97 ++++++++++++++-------------------------------- docs/bench/main.go | 31 +++++---------- 2 files changed, 39 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 2b40b12..e5a8548 100644 --- a/README.md +++ b/README.md @@ -95,76 +95,39 @@ That design choice doesn't matter much for some single operations. It matters a | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 249ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 249ns / 234ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 138ns / 1.0µs | **7.55x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 7.9µs / 7.9µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.2µs / 8.5µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 18.7µs / 42.9µs | **2.29x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | -| **Each** | 248ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 626ns / 1.0µs | **1.64x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **All** | 260ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 255ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 139ns / 1.1µs | **7.76x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 250ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.1µs / 7.9µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.1µs / 8.0µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 18.9µs / 42.8µs | **2.27x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 256ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 628ns / 1.0µs | **1.63x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | | **First** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 7.9µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 10.8µs / 10.5µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **FirstWhere** | 252ns / 234ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 8.0µs / 8.4µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 253ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 10.7µs / 10.5µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | | **Last** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 829ns / 788ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **Max** | 265ns / 249ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 264ns / 232ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 767ns / 1.2µs | **1.62x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | -| **Reduce (sum)** | 248ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 225ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 3.8µs / 5.2µs | **1.37x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 11ns / 709ns | **65.92x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 11ns / 708ns | **65.15x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 264ns / 232ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 21ns / 0ns | 0.01x | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.5µs / 7.8µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 17.1µs / 17.5µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Map** | 825ns / 794ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **Max** | 254ns / 232ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 255ns / 236ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 260ns / 246ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 803ns / 1.3µs | **1.67x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | +| **Reduce (sum)** | 254ns / 239ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 223ns / 231ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 3.8µs / 5.2µs | **1.36x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 11ns / 711ns | **66.10x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 11ns / 708ns | **65.08x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 255ns / 237ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 22ns / 0ns | 0.01x | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.5µs / 7.7µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 17.1µs / 17.3µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | | **Unique** | 6.3µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.6µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.4µs / 3.1µs | **2.21x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.0µs / 2.9µs | **2.85x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | - -#### Explicit Copy (CopyOf) vs lo -| Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | -|---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 840ns / 233ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Any** | 1.0µs / 236ns | 0.24x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Chunk** | 799ns / 1.1µs | **1.34x** | 9.5KB / 9.3KB | ≈ | 3 / 51 | -| **Contains** | 833ns / 234ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **CountBy** | 8.4µs / 7.9µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 13 / 11 | -| **CountByValue** | 8.7µs / 8.5µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 13 / 11 | -| **Difference** | 20.3µs / 43.5µs | **2.14x** | 98.5KB / 108.8KB | **1.10x less** | 16 / 43 | -| **Each** | 831ns / 232ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Filter** | 1.1µs / 1.0µs | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **First** | 677ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **FirstWhere** | 842ns / 234ns | 0.28x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **GroupBySlice** | 8.5µs / 8.3µs | ≈ | 29.2KB / 21.0KB | 0.72x more | 85 / 83 | -| **IndexWhere** | 855ns / 234ns | 0.27x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Intersect** | 11.8µs / 10.7µs | ≈ | 27.8KB / 11.4KB | 0.41x more | 24 / 19 | -| **Last** | 688ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Map** | 1.4µs / 799ns | 0.55x | 16.4KB / 8.2KB | 0.50x more | 3 / 1 | -| **Max** | 848ns / 232ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Min** | 846ns / 232ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **None** | 911ns / 234ns | 0.26x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Pipeline F→M→T→R** | 1.3µs / 1.3µs | ≈ | 12.3KB / 12.3KB | ≈ | 4 / 2 | -| **Reduce (sum)** | 838ns / 240ns | 0.29x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Reverse** | 780ns / 233ns | 0.30x | 8.2KB / 0B | ∞x more | 2 / 0 | -| **Shuffle** | 4.2µs / 5.2µs | **1.24x** | 16.4KB / 0B | ∞x more | 4 / 0 | -| **Skip** | 680ns / 714ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **SkipLast** | 677ns / 715ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **Sum** | 847ns / 233ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Take** | 693ns / 0ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **ToMap** | 8.1µs / 7.8µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 7 / 6 | -| **Union** | 18.1µs / 17.5µs | ≈ | 106.7KB / 90.3KB | 0.85x more | 15 / 10 | -| **Unique** | 7.0µs / 6.4µs | ≈ | 53.4KB / 45.1KB | 0.85x more | 8 / 6 | -| **UniqueBy** | 7.3µs / 6.3µs | 0.87x | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | -| **Zip** | 2.6µs / 3.1µs | **1.19x** | 32.8KB / 16.4KB | 0.50x more | 5 / 1 | -| **ZipWith** | 2.3µs / 2.9µs | **1.27x** | 24.6KB / 8.2KB | 0.33x more | 5 / 1 | +| **UniqueBy** | 6.7µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.4µs / 3.1µs | **2.20x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.0µs / 2.9µs | **2.84x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | ## How to read the benchmarks diff --git a/docs/bench/main.go b/docs/bench/main.go index a690d84..ff40f2a 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -39,11 +39,9 @@ func main() { start := time.Now() only := parseOnly(*onlyFlag) borrowResults := runBenches(only, benchBorrow) - copyResults := runBenches(only, benchCopy) borrowTable := renderTable(borrowResults) - copyTable := renderTable(copyResults) - if err := updateReadme(borrowTable, copyTable); err != nil { + if err := updateReadme(borrowTable); err != nil { fmt.Println("Error:", err) os.Exit(1) } @@ -62,7 +60,6 @@ type benchMode string const ( benchBorrow benchMode = "borrow" - benchCopy benchMode = "copy" ) var ( @@ -74,9 +71,6 @@ var ( func setBenchMode(mode benchMode) { currentMode = mode switch mode { - case benchCopy: - ctorInts = collection.CopyOf[int] - ctorNumericInt = collection.CopyOfNumeric[int] default: ctorInts = collection.New[int] ctorNumericInt = collection.NewNumeric[int] @@ -1072,7 +1066,7 @@ func collectionInputForMutating(src []int) []int { // README injection // ---------------------------------------------------------------------------- -func updateReadme(borrowTable, copyTable string) error { +func updateReadme(borrowTable string) error { root, err := findRoot() if err != nil { return err @@ -1084,7 +1078,7 @@ func updateReadme(borrowTable, copyTable string) error { return err } - out, err := replaceSection(string(data), borrowTable, copyTable) + out, err := replaceSection(string(data), borrowTable) if err != nil { return err } @@ -1092,7 +1086,7 @@ func updateReadme(borrowTable, copyTable string) error { return os.WriteFile(readmePath, []byte(out), 0o644) } -func replaceSection(readme, borrowTable, copyTable string) (string, error) { +func replaceSection(readme, borrowTable string) (string, error) { start := strings.Index(readme, benchStart) end := strings.Index(readme, benchEnd) if start == -1 || end == -1 || end < start { @@ -1100,7 +1094,7 @@ func replaceSection(readme, borrowTable, copyTable string) (string, error) { } section := readme[start+len(benchStart) : end] - updated, err := replaceBenchTables(section, borrowTable, copyTable) + updated, err := replaceBenchTable(section, borrowTable) if err != nil { return "", err } @@ -1112,7 +1106,7 @@ func replaceSection(readme, borrowTable, copyTable string) (string, error) { return buf.String(), nil } -func replaceBenchTables(section, borrowTable, copyTable string) (string, error) { +func replaceBenchTable(section, borrowTable string) (string, error) { lines := strings.Split(section, "\n") tableStarts := []int{} for i, line := range lines { @@ -1120,8 +1114,8 @@ func replaceBenchTables(section, borrowTable, copyTable string) (string, error) tableStarts = append(tableStarts, i) } } - if len(tableStarts) < 2 { - return "", fmt.Errorf("expected two benchmark tables in section") + if len(tableStarts) == 0 { + return "", fmt.Errorf("expected benchmark table in section") } tableEnds := make([]int, len(tableStarts)) @@ -1138,7 +1132,6 @@ func replaceBenchTables(section, borrowTable, copyTable string) (string, error) } firstStart, firstEnd := tableStarts[0], tableEnds[0] - secondStart, secondEnd := tableStarts[1], tableEnds[1] var buf bytes.Buffer if firstStart > 0 { @@ -1147,13 +1140,7 @@ func replaceBenchTables(section, borrowTable, copyTable string) (string, error) } buf.WriteString(strings.TrimSpace(borrowTable)) buf.WriteString("\n") - buf.WriteString(strings.Join(lines[firstEnd:secondStart], "\n")) - if buf.Len() > 0 && !strings.HasSuffix(buf.String(), "\n") { - buf.WriteString("\n") - } - buf.WriteString(strings.TrimSpace(copyTable)) - buf.WriteString("\n") - buf.WriteString(strings.Join(lines[secondEnd:], "\n")) + buf.WriteString(strings.Join(lines[firstEnd:], "\n")) return buf.String(), nil } From 3bb4bf9260c104f5376b262a47fbd970abe05e5a Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 23:30:51 -0600 Subject: [PATCH 11/30] docs: benchmarks --- README.md | 66 +++++++++++++++++++++++----------------------- docs/bench/main.go | 10 ++++++- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index e5a8548..1e646ad 100644 --- a/README.md +++ b/README.md @@ -95,39 +95,39 @@ That design choice doesn't matter much for some single operations. It matters a | Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 260ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 255ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 139ns / 1.1µs | **7.76x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 250ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.1µs / 7.9µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.1µs / 8.0µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 18.9µs / 42.8µs | **2.27x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | -| **Each** | 256ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 628ns / 1.0µs | **1.63x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **First** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 252ns / 234ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 8.0µs / 8.4µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 253ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 10.7µs / 10.5µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | -| **Last** | 11ns / 0ns | 0.02x | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 825ns / 794ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **Max** | 254ns / 232ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 255ns / 236ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 260ns / 246ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 803ns / 1.3µs | **1.67x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | -| **Reduce (sum)** | 254ns / 239ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 223ns / 231ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 3.8µs / 5.2µs | **1.36x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 11ns / 711ns | **66.10x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 11ns / 708ns | **65.08x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 255ns / 237ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 22ns / 0ns | 0.01x | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.5µs / 7.7µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 17.1µs / 17.3µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | -| **Unique** | 6.3µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.7µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.4µs / 3.1µs | **2.20x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.0µs / 2.9µs | **2.84x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **All** | 248ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 140ns / 1.1µs | **7.54x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.0µs / 8.0µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.1µs / 8.2µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 18.9µs / 42.8µs | **2.26x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 253ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 632ns / 1.0µs | **1.63x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **First** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **FirstWhere** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 7.9µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.0µs / 10.6µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **Last** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Map** | 807ns / 789ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **Max** | 255ns / 232ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 256ns / 237ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 249ns / 239ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 804ns / 1.2µs | **1.55x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | +| **Reduce (sum)** | 253ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 227ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 3.8µs / 5.2µs | **1.38x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 11ns / 715ns | **66.50x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 11ns / 716ns | **66.64x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 267ns / 239ns | 0.90x | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 21ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.5µs / 8.0µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 16.9µs / 17.7µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Unique** | 6.4µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **UniqueBy** | 6.6µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.4µs / 3.2µs | **2.24x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.0µs / 3.1µs | **3.11x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | ## How to read the benchmarks diff --git a/docs/bench/main.go b/docs/bench/main.go index ff40f2a..07d83b5 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -964,6 +964,8 @@ func renderTable(results []benchResult) string { func formatNs(ns float64) string { switch { + case ns < 1: + return "<1ns" case ns >= 1e6: return fmt.Sprintf("%.1fms", ns/1e6) case ns >= 1e3: @@ -997,9 +999,15 @@ func formatDurationNs(ns float64) string { } } -const wrapperEpsilon = 0.10 // ±10% wrapper overhead tolerance +const ( + wrapperEpsilon = 0.10 // ±10% wrapper overhead tolerance + benchRatioNoiseNs = 50.0 +) func formatRatio(lo, col float64) string { + if lo < benchRatioNoiseNs && col < benchRatioNoiseNs { + return "≈" + } if col == 0 { return "∞" } From 4b12a498e0512d1206d7cb19bddd911e5ee1d2aa Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 23:40:56 -0600 Subject: [PATCH 12/30] docs: benchmarks --- README.md | 66 +++++++++++++++++++++++----------------------- docs/bench/main.go | 2 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 1e646ad..9d6179c 100644 --- a/README.md +++ b/README.md @@ -93,41 +93,41 @@ That design choice doesn't matter much for some single operations. It matters a #### Default (New, borrowed) vs lo -| Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) | +| Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 248ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 140ns / 1.1µs | **7.54x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.0µs / 8.0µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.1µs / 8.2µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 18.9µs / 42.8µs | **2.26x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | -| **Each** | 253ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 632ns / 1.0µs | **1.63x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **First** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 7.9µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 11.0µs / 10.6µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **All** | 253ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 252ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 143ns / 1.1µs | **7.60x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 253ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.5µs / 8.2µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.4µs / 8.2µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 19.1µs / 43.2µs | **2.26x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 258ns / 238ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 647ns / 1.1µs | **1.64x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **First** | 12ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **FirstWhere** | 255ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 8.5µs / 8.8µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 260ns / 243ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.0µs / 10.7µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | | **Last** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 807ns / 789ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **Max** | 255ns / 232ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 256ns / 237ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 249ns / 239ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 804ns / 1.2µs | **1.55x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | -| **Reduce (sum)** | 253ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 227ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 3.8µs / 5.2µs | **1.38x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 11ns / 715ns | **66.50x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 11ns / 716ns | **66.64x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 267ns / 239ns | 0.90x | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 21ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.5µs / 8.0µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 16.9µs / 17.7µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | -| **Unique** | 6.4µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.6µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.4µs / 3.2µs | **2.24x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.0µs / 3.1µs | **3.11x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **Map** | 843ns / 814ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **Max** | 253ns / 230ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 253ns / 235ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 251ns / 251ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 870ns / 1.3µs | **1.44x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | +| **Reduce (sum)** | 258ns / 242ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 240ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 3.9µs / 5.3µs | **1.37x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 12ns / 753ns | **63.38x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 11ns / 716ns | **62.99x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 255ns / 235ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 22ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.5µs / 7.8µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 17.2µs / 17.9µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Unique** | 6.4µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **UniqueBy** | 6.8µs / 6.5µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.4µs / 3.2µs | **2.25x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.0µs / 3.2µs | **3.12x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | ## How to read the benchmarks diff --git a/docs/bench/main.go b/docs/bench/main.go index 07d83b5..23c4167 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -919,7 +919,7 @@ func renderTable(results []benchResult) string { } var buf bytes.Buffer - buf.WriteString("| Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) |\n") + buf.WriteString("| Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) |\n") buf.WriteString("|---:|----------------|:--:|------------------|:--:|--------------------|\n") names := make([]string, 0, len(byName)) From ae8d63ebf38aa76e2334d444fdeb7e604a887eeb Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 1 Jan 2026 23:51:00 -0600 Subject: [PATCH 13/30] docs: benchmarks --- BENCHMARKS.md | 39 ++++++++ README.md | 91 +++++++++++-------- docs/bench/main.go | 218 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 273 insertions(+), 75 deletions(-) create mode 100644 BENCHMARKS.md diff --git a/BENCHMARKS.md b/BENCHMARKS.md new file mode 100644 index 0000000..fba8161 --- /dev/null +++ b/BENCHMARKS.md @@ -0,0 +1,39 @@ +# Benchmarks + +Raw results for `collection.New` (borrowed) vs `lo`. + +| Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | +|---:|----------------|:--:|------------------|:--:|--------------------| +| **All** | 254ns / 238ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 253ns / 248ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 139ns / 1.0µs | **7.55x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 250ns / 235ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.4µs / 8.5µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.4µs / 8.3µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 19.7µs / 44.5µs | **2.26x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 254ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 658ns / 1.0µs | **1.56x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **First** | 12ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **FirstWhere** | 251ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 8.3µs / 8.7µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 252ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.3µs / 10.9µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **Last** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Map** | 826ns / 791ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | +| **Max** | 256ns / 237ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 270ns / 233ns | 0.87x | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 252ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 758ns / 1.3µs | **1.66x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | +| **Reduce (sum)** | 248ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 240ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 4.0µs / 5.3µs | **1.33x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 12ns / 739ns | **64.23x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 12ns / 765ns | **63.73x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 266ns / 234ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 22ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.8µs / 8.0µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 17.2µs / 17.9µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Unique** | 6.3µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **UniqueBy** | 6.7µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.4µs / 3.3µs | **2.28x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.1µs / 3.2µs | **3.00x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | diff --git a/README.md b/README.md index 9d6179c..6dcbaf2 100644 --- a/README.md +++ b/README.md @@ -91,43 +91,60 @@ That design choice doesn't matter much for some single operations. It matters a -#### Default (New, borrowed) vs lo - -| Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | -|---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 253ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 252ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 143ns / 1.1µs | **7.60x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 253ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.5µs / 8.2µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.4µs / 8.2µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 19.1µs / 43.2µs | **2.26x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | -| **Each** | 258ns / 238ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 647ns / 1.1µs | **1.64x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **First** | 12ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 255ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 8.5µs / 8.8µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 260ns / 243ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 11.0µs / 10.7µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | -| **Last** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 843ns / 814ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **Max** | 253ns / 230ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 253ns / 235ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 251ns / 251ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 870ns / 1.3µs | **1.44x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | -| **Reduce (sum)** | 258ns / 242ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 240ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 3.9µs / 5.3µs | **1.37x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 12ns / 753ns | **63.38x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 11ns / 716ns | **62.99x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 255ns / 235ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 22ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.5µs / 7.8µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 17.2µs / 17.9µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | -| **Unique** | 6.4µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.8µs / 6.5µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.4µs / 3.2µs | **2.25x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.0µs / 3.2µs | **3.12x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +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 | +|---:|:-----------:|:------:|:------:| +| **Map** | ≈ | +24B | +1 | +| **Chunk** | **7.55x** | -8.0KB | -49 | +| **Take** | ≈ | +48B | +2 | +| **Skip** | **64.23x** | -8.2KB | ≈ | +| **SkipLast** | **63.73x** | -8.2KB | ≈ | +| **Zip** | **2.28x** | +48B | +2 | +| **ZipWith** | **3.00x** | +48B | +2 | +| **Unique** | ≈ | +24B | +1 | +| **UniqueBy** | ≈ | +48B | +2 | +| **Union** | ≈ | +72B | +3 | +| **Intersect** | ≈ | +72B | +3 | +| **Difference** | **2.26x** | -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.66x** | -8.1KB | +1 | + +#### Mutating ops + +| Op | Speed vs lo | Memory | Allocs | +|---:|:-----------:|:------:|:------:| +| **Filter** | **1.56x** | -8.2KB | ≈ | +| **Reverse** | ≈ | +24B | +1 | +| **Shuffle** | **1.33x** | +8.2KB | +3 | ## How to read the benchmarks diff --git a/docs/bench/main.go b/docs/bench/main.go index 23c4167..0185546 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -39,9 +39,14 @@ func main() { start := time.Now() only := parseOnly(*onlyFlag) borrowResults := runBenches(only, benchBorrow) - borrowTable := renderTable(borrowResults) + condensed := renderCondensedTables(borrowResults) + rawTable := renderTable(borrowResults) - if err := updateReadme(borrowTable); err != nil { + if err := updateReadme(condensed); err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + if err := updateBenchmarksFile(rawTable); err != nil { fmt.Println("Error:", err) os.Exit(1) } @@ -962,6 +967,112 @@ func renderTable(results []benchResult) string { return strings.TrimSpace(buf.String()) } +type benchGroup struct { + name string + ops []string +} + +func renderCondensedTables(results []benchResult) string { + byName := map[string]map[string]benchResult{} + for _, r := range results { + if _, ok := byName[r.name]; !ok { + byName[r.name] = map[string]benchResult{} + } + byName[r.name][r.impl] = r + } + + groups := []benchGroup{ + { + name: "Read-only scalar ops (wrapper overhead only)", + ops: []string{ + "All", + "Any", + "None", + "First", + "Last", + "FirstWhere", + "IndexWhere", + "Contains", + "Reduce (sum)", + "Sum", + "Min", + "Max", + "Each", + }, + }, + { + name: "Transforming ops", + ops: []string{ + "Map", + "Chunk", + "Take", + "Skip", + "SkipLast", + "Zip", + "ZipWith", + "Unique", + "UniqueBy", + "Union", + "Intersect", + "Difference", + "GroupBySlice", + "CountBy", + "CountByValue", + "ToMap", + }, + }, + { + name: "Pipelines", + ops: []string{ + "Pipeline F→M→T→R", + }, + }, + { + name: "Mutating ops", + ops: []string{ + "Filter", + "Reverse", + "Shuffle", + }, + }, + } + + var buf bytes.Buffer + buf.WriteString("Full raw tables: see `BENCHMARKS.md`.\n\n") + + for _, group := range groups { + rows := make([]string, 0, len(group.ops)) + for _, name := range group.ops { + entry, ok := byName[name] + if !ok { + continue + } + col := entry["collection"] + loRes := entry["lo"] + + wrapperOnly := group.name == "Read-only scalar ops (wrapper overhead only)" + allowBold := group.name == "Pipelines" || group.name == "Transforming ops" || group.name == "Mutating ops" + speed := formatSpeed(loRes.nsPerOp, col.nsPerOp, allowBold, wrapperOnly) + mem := formatDeltaBytes(col.bytesPerOp, loRes.bytesPerOp) + allocs := formatDeltaAllocs(col.allocsPerOp, loRes.allocsPerOp) + + rows = append(rows, fmt.Sprintf("| **%s** | %s | %s | %s |", name, speed, mem, allocs)) + } + + if len(rows) == 0 { + continue + } + + buf.WriteString(fmt.Sprintf("#### %s\n\n", group.name)) + buf.WriteString("| Op | Speed vs lo | Memory | Allocs |\n") + buf.WriteString("|---:|:-----------:|:------:|:------:|\n") + buf.WriteString(strings.Join(rows, "\n")) + buf.WriteString("\n\n") + } + + return strings.TrimSpace(buf.String()) +} + func formatNs(ns float64) string { switch { case ns < 1: @@ -1000,8 +1111,9 @@ func formatDurationNs(ns float64) string { } const ( - wrapperEpsilon = 0.10 // ±10% wrapper overhead tolerance - benchRatioNoiseNs = 50.0 + wrapperEpsilon = 0.10 // ±10% wrapper overhead tolerance + benchRatioNoiseNs = 50.0 + wrapperOnlyEpsilon = 0.15 ) func formatRatio(lo, col float64) string { @@ -1026,6 +1138,29 @@ func formatRatio(lo, col float64) string { return out } +func formatSpeed(lo, col float64, allowBold bool, wrapperOnly bool) string { + if lo < benchRatioNoiseNs && col < benchRatioNoiseNs { + return "≈" + } + if col == 0 { + return "∞" + } + + ratio := lo / col + if wrapperOnly && ratio >= 1-wrapperOnlyEpsilon && ratio <= 1+wrapperOnlyEpsilon { + return "≈" + } + if ratio >= 1-wrapperEpsilon && ratio <= 1+wrapperEpsilon { + return "≈" + } + + out := fmt.Sprintf("%.2fx", ratio) + if ratio > 1 && allowBold { + return fmt.Sprintf("**%s**", out) + } + return out +} + func formatRatioBytes(lo, col int64) string { switch { case lo == 0 && col == 0: @@ -1062,6 +1197,28 @@ func formatInt(v int64) string { } } +func formatDeltaBytes(col, lo int64) string { + if col == lo { + return "≈" + } + diff := col - lo + if diff > 0 { + return fmt.Sprintf("+%s", formatBytes(diff)) + } + return fmt.Sprintf("-%s", formatBytes(-diff)) +} + +func formatDeltaAllocs(col, lo int64) string { + if col == lo { + return "≈" + } + diff := col - lo + if diff > 0 { + return fmt.Sprintf("+%d", diff) + } + return fmt.Sprintf("%d", diff) +} + func collectionInputForMutating(src []int) []int { if currentMode == benchBorrow { copy(workA, src) @@ -1074,7 +1231,7 @@ func collectionInputForMutating(src []int) []int { // README injection // ---------------------------------------------------------------------------- -func updateReadme(borrowTable string) error { +func updateReadme(condensed string) error { root, err := findRoot() if err != nil { return err @@ -1086,7 +1243,7 @@ func updateReadme(borrowTable string) error { return err } - out, err := replaceSection(string(data), borrowTable) + out, err := replaceSection(string(data), condensed) if err != nil { return err } @@ -1094,7 +1251,7 @@ func updateReadme(borrowTable string) error { return os.WriteFile(readmePath, []byte(out), 0o644) } -func replaceSection(readme, borrowTable string) (string, error) { +func replaceSection(readme, condensed string) (string, error) { start := strings.Index(readme, benchStart) end := strings.Index(readme, benchEnd) if start == -1 || end == -1 || end < start { @@ -1102,7 +1259,7 @@ func replaceSection(readme, borrowTable string) (string, error) { } section := readme[start+len(benchStart) : end] - updated, err := replaceBenchTable(section, borrowTable) + updated, err := replaceBenchTable(section, condensed) if err != nil { return "", err } @@ -1114,42 +1271,27 @@ func replaceSection(readme, borrowTable string) (string, error) { return buf.String(), nil } -func replaceBenchTable(section, borrowTable string) (string, error) { - lines := strings.Split(section, "\n") - tableStarts := []int{} - for i, line := range lines { - if strings.HasPrefix(strings.TrimSpace(line), "| Op |") { - tableStarts = append(tableStarts, i) - } - } - if len(tableStarts) == 0 { - return "", fmt.Errorf("expected benchmark table in section") +func replaceBenchTable(section, condensed string) (string, error) { + trimmed := strings.TrimSpace(condensed) + if trimmed == "" { + return "", fmt.Errorf("condensed benchmark content is empty") } + return "\n\n" + trimmed + "\n", nil +} - tableEnds := make([]int, len(tableStarts)) - for i, start := range tableStarts { - end := start - for end < len(lines) { - line := strings.TrimSpace(lines[end]) - if line == "" { - break - } - end++ - } - tableEnds[i] = end +func updateBenchmarksFile(rawTable string) error { + root, err := findRoot() + if err != nil { + return err } - firstStart, firstEnd := tableStarts[0], tableEnds[0] - + path := filepath.Join(root, "BENCHMARKS.md") var buf bytes.Buffer - if firstStart > 0 { - buf.WriteString(strings.Join(lines[:firstStart], "\n")) - buf.WriteString("\n") - } - buf.WriteString(strings.TrimSpace(borrowTable)) + buf.WriteString("# Benchmarks\n\n") + buf.WriteString("Raw results for `collection.New` (borrowed) vs `lo`.\n\n") + buf.WriteString(rawTable) buf.WriteString("\n") - buf.WriteString(strings.Join(lines[firstEnd:], "\n")) - return buf.String(), nil + return os.WriteFile(path, buf.Bytes(), 0o644) } // ---------------------------------------------------------------------------- From 0804a7c12fcf2ac626b0fa80bf97d14ba71d4a50 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 00:04:51 -0600 Subject: [PATCH 14/30] feat: map mutability --- BENCHMARKS.md | 62 ++++++++++++++++++++++---------------------- README.md | 24 ++++++++--------- constructor_test.go | 9 +++++++ docs/bench/main.go | 2 +- examples/map/main.go | 2 +- map.go | 9 +++---- map_test.go | 20 +++++++++++--- pipe_test.go | 2 +- 8 files changed, 75 insertions(+), 55 deletions(-) diff --git a/BENCHMARKS.md b/BENCHMARKS.md index fba8161..a8cf97d 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -4,36 +4,36 @@ Raw results for `collection.New` (borrowed) vs `lo`. | Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 254ns / 238ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 253ns / 248ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 139ns / 1.0µs | **7.55x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 250ns / 235ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.4µs / 8.5µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.4µs / 8.3µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 19.7µs / 44.5µs | **2.26x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | -| **Each** | 254ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 658ns / 1.0µs | **1.56x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **First** | 12ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 251ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 8.3µs / 8.7µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 252ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 11.3µs / 10.9µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **All** | 249ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 142ns / 1.0µs | **7.39x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 250ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.3µs / 8.3µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.2µs / 8.0µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 20.8µs / 49.1µs | **2.37x** | 82.2KB / 134.0KB | **1.63x less** | 14 / 55 | +| **Each** | 248ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 757ns / 1.1µs | **1.41x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **First** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **FirstWhere** | 249ns / 234ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 9.8µs / 10.4µs | ≈ | 22.2KB / 22.1KB | ≈ | 140 / 139 | +| **IndexWhere** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.0µs / 10.7µs | ≈ | 9.4KB / 9.4KB | ≈ | 15 / 12 | | **Last** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 826ns / 791ns | ≈ | 8.2KB / 8.2KB | ≈ | 2 / 1 | -| **Max** | 256ns / 237ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 270ns / 233ns | 0.87x | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 252ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 758ns / 1.3µs | **1.66x** | 4.1KB / 12.3KB | **2.97x less** | 3 / 2 | -| **Reduce (sum)** | 248ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 240ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 4.0µs / 5.3µs | **1.33x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 12ns / 739ns | **64.23x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 12ns / 765ns | **63.73x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 266ns / 234ns | 0.88x | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 22ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.8µs / 8.0µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 17.2µs / 17.9µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Map** | 295ns / 790ns | **2.68x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Max** | 264ns / 238ns | 0.90x | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 255ns / 234ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 248ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 503ns / 1.2µs | **2.47x** | 48B / 12.3KB | **256.00x less** | 2 / 2 | +| **Reduce (sum)** | 253ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 237ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 3.9µs / 5.6µs | **1.44x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 11ns / 712ns | **63.96x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 11ns / 708ns | **63.64x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 255ns / 232ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 21ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.9µs / 8.1µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 18.0µs / 18.6µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | | **Unique** | 6.3µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.7µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.4µs / 3.3µs | **2.28x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.1µs / 3.2µs | **3.00x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **UniqueBy** | 6.6µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.4µs / 3.2µs | **2.28x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.0µs / 3.0µs | **2.96x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | diff --git a/README.md b/README.md index 6dcbaf2..f1b2860 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Latest tag - Tests + Tests Go Report Card

@@ -115,18 +115,17 @@ Full raw tables: see `BENCHMARKS.md`. | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Map** | ≈ | +24B | +1 | -| **Chunk** | **7.55x** | -8.0KB | -49 | +| **Chunk** | **7.39x** | -8.0KB | -49 | | **Take** | ≈ | +48B | +2 | -| **Skip** | **64.23x** | -8.2KB | ≈ | -| **SkipLast** | **63.73x** | -8.2KB | ≈ | +| **Skip** | **63.96x** | -8.2KB | ≈ | +| **SkipLast** | **63.64x** | -8.2KB | ≈ | | **Zip** | **2.28x** | +48B | +2 | -| **ZipWith** | **3.00x** | +48B | +2 | +| **ZipWith** | **2.96x** | +48B | +2 | | **Unique** | ≈ | +24B | +1 | | **UniqueBy** | ≈ | +48B | +2 | | **Union** | ≈ | +72B | +3 | | **Intersect** | ≈ | +72B | +3 | -| **Difference** | **2.26x** | -26.7KB | -29 | +| **Difference** | **2.37x** | -51.9KB | -41 | | **GroupBySlice** | ≈ | +24B | +1 | | **CountBy** | ≈ | +24B | +1 | | **CountByValue** | ≈ | +24B | +1 | @@ -136,15 +135,16 @@ Full raw tables: see `BENCHMARKS.md`. | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Pipeline F→M→T→R** | **1.66x** | -8.1KB | +1 | +| **Pipeline F→M→T→R** | **2.47x** | -12.2KB | ≈ | #### Mutating ops | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Filter** | **1.56x** | -8.2KB | ≈ | +| **Map** | **2.68x** | -8.2KB | ≈ | +| **Filter** | **1.41x** | -8.2KB | ≈ | | **Reverse** | ≈ | +24B | +1 | -| **Shuffle** | **1.33x** | +8.2KB | +3 | +| **Shuffle** | **1.44x** | +8.2KB | +3 | ## How to read the benchmarks @@ -3497,9 +3497,9 @@ collection.Dump(names) // ] ``` -### Map · immutable · fluent +### Map · mutable · fluent -Map applies a same-type transformation and returns a new collection. +Map applies a same-type transformation in place. _Example: integers_ diff --git a/constructor_test.go b/constructor_test.go index 8fe6fb4..9b0d467 100644 --- a/constructor_test.go +++ b/constructor_test.go @@ -73,6 +73,15 @@ func TestCopyOfNumeric_CopiesInputSlice(t *testing.T) { } } +func TestCopyOfNumeric_PreservesNilSlice(t *testing.T) { + var items []int + c := CopyOfNumeric(items) + + if c.Items() != nil { + t.Fatalf("CopyOfNumeric should preserve nil slice") + } +} + func TestSelectionOps_ShareBackingSlice(t *testing.T) { items := []int{1, 2, 3, 4} c := New(items) diff --git a/docs/bench/main.go b/docs/bench/main.go index 0185546..c30312c 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -1003,7 +1003,6 @@ func renderCondensedTables(results []benchResult) string { { name: "Transforming ops", ops: []string{ - "Map", "Chunk", "Take", "Skip", @@ -1030,6 +1029,7 @@ func renderCondensedTables(results []benchResult) string { { name: "Mutating ops", ops: []string{ + "Map", "Filter", "Reverse", "Shuffle", diff --git a/examples/map/main.go b/examples/map/main.go index ee94baa..005a232 100644 --- a/examples/map/main.go +++ b/examples/map/main.go @@ -9,7 +9,7 @@ import ( ) func main() { - // Map applies a same-type transformation and returns a new collection. + // Map applies a same-type transformation in place. // Example: integers c := collection.New([]int{1, 2, 3}) diff --git a/map.go b/map.go index 0ca1c16..37149d7 100644 --- a/map.go +++ b/map.go @@ -1,8 +1,8 @@ package collection -// Map applies a same-type transformation and returns a new collection. +// Map applies a same-type transformation in place. // @group Transformation -// @behavior immutable +// @behavior mutable // @fluent true // // Use this when you're transforming T -> T (e.g., enrichment, normalization). @@ -66,9 +66,8 @@ package collection // // } // // ] func (c *Collection[T]) Map(fn func(T) T) *Collection[T] { - out := make([]T, len(c.items)) for i, v := range c.items { - out[i] = fn(v) + c.items[i] = fn(v) } - return New(out) + return c } diff --git a/map_test.go b/map_test.go index da2d4dc..76dd367 100644 --- a/map_test.go +++ b/map_test.go @@ -18,8 +18,12 @@ func TestMap_Ints(t *testing.T) { t.Fatalf("expected %v, got %v", expected, mapped.items) } - if !reflect.DeepEqual(c.Items(), []int{1, 2, 3}) { - t.Fatalf("Map should not mutate original collection") + if mapped != c { + t.Fatalf("Map should return the same collection") + } + + if !reflect.DeepEqual(c.Items(), expected) { + t.Fatalf("Map should mutate original collection") } } @@ -48,8 +52,12 @@ func TestMap_Structs(t *testing.T) { t.Fatalf("expected %v, got %v", expected, mapped.items) } - if !reflect.DeepEqual(c.Items(), []User{{1, "Chris"}, {2, "Van"}}) { - t.Fatalf("Map should not mutate original collection") + if mapped != c { + t.Fatalf("Map should return the same collection") + } + + if !reflect.DeepEqual(c.Items(), expected) { + t.Fatalf("Map should mutate original collection") } } @@ -63,4 +71,8 @@ func TestMap_Empty(t *testing.T) { if len(mapped.items) != 0 { t.Fatalf("expected empty slice, got %v", mapped.items) } + + if mapped != c { + t.Fatalf("Map should return the same collection") + } } diff --git a/pipe_test.go b/pipe_test.go index 3a78f53..60c420c 100644 --- a/pipe_test.go +++ b/pipe_test.go @@ -40,7 +40,7 @@ func TestPipe_IsNonMutating(t *testing.T) { c := New([]int{1, 2, 3}) _ = Pipe(c, func(col *Collection[int]) *Collection[int] { - return col.Map(func(v int) int { return v * 2 }) + return col }) if !reflect.DeepEqual(c.Items(), []int{1, 2, 3}) { From 1b5d22ef52af32188f4638a4454d99c97f1ef969 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 01:16:18 -0600 Subject: [PATCH 15/30] chore: v2 readiness --- BENCHMARKS.md | 104 +++++++++++------ README.md | 199 +++++++++++++++++++-------------- after.go | 3 +- all.go | 2 +- any.go | 2 +- append.go | 3 +- at.go | 2 +- avg.go | 2 +- before.go | 3 +- chunk.go | 2 +- clone.go | 3 +- collection.go | 70 +----------- concat.go | 3 +- constructor_test.go | 40 ------- contains.go | 2 +- count.go | 2 +- count_by.go | 4 +- difference.go | 3 +- doc.go | 5 + docs/backfill/main.go | 96 ++++++++-------- docs/bench/main.go | 53 ++++++--- docs/readme/main.go | 16 +-- dump.go | 9 +- each.go | 3 +- examples/clone/main.go | 3 +- examples/copyof/main.go | 22 ---- examples/copyofnumeric/main.go | 22 ---- examples/map/main.go | 2 +- examples/take/main.go | 3 +- examples/takelast/main.go | 3 +- examples/takeuntil/main.go | 3 +- filter.go | 3 +- first.go | 2 +- first_where.go | 2 +- from_map.go | 3 +- group_by.go | 2 +- group_by_slice.go | 2 +- index_where.go | 2 +- intersect.go | 3 +- is_empty.go | 2 +- last.go | 2 +- last_where.go | 2 +- map.go | 6 +- max.go | 2 +- max_by.go | 2 +- median.go | 2 +- merge.go | 3 +- min.go | 2 +- min_by.go | 2 +- mode.go | 2 +- multiply.go | 3 +- none.go | 2 +- partition.go | 3 +- pipe.go | 2 +- pluck.go | 3 +- pop.go | 4 +- prepend.go | 3 +- reduce.go | 2 +- reverse.go | 3 +- shuffle.go | 3 +- skip.go | 3 +- skip_last.go | 3 +- sort.go | 3 +- sum.go | 2 +- symmetric_difference.go | 3 +- take.go | 3 +- take_last.go | 3 +- take_until.go | 6 +- tap.go | 3 +- terminal_allocs_test.go | 117 +++++++++++++++++++ times.go | 3 +- to_json.go | 4 +- to_map.go | 2 +- to_map_kv.go | 2 +- transform.go | 2 +- union.go | 3 +- unique.go | 3 +- unique_by.go | 3 +- unique_comparable.go | 3 +- window.go | 3 +- zip.go | 6 +- 81 files changed, 523 insertions(+), 420 deletions(-) create mode 100644 doc.go delete mode 100644 examples/copyof/main.go delete mode 100644 examples/copyofnumeric/main.go create mode 100644 terminal_allocs_test.go diff --git a/BENCHMARKS.md b/BENCHMARKS.md index a8cf97d..0526fa2 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -4,36 +4,74 @@ Raw results for `collection.New` (borrowed) vs `lo`. | Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 249ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 142ns / 1.0µs | **7.39x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 250ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.3µs / 8.3µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.2µs / 8.0µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 20.8µs / 49.1µs | **2.37x** | 82.2KB / 134.0KB | **1.63x less** | 14 / 55 | -| **Each** | 248ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 757ns / 1.1µs | **1.41x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **First** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 249ns / 234ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 9.8µs / 10.4µs | ≈ | 22.2KB / 22.1KB | ≈ | 140 / 139 | -| **IndexWhere** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 11.0µs / 10.7µs | ≈ | 9.4KB / 9.4KB | ≈ | 15 / 12 | -| **Last** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 295ns / 790ns | **2.68x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Max** | 264ns / 238ns | 0.90x | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 255ns / 234ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 248ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 503ns / 1.2µs | **2.47x** | 48B / 12.3KB | **256.00x less** | 2 / 2 | -| **Reduce (sum)** | 253ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 237ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 3.9µs / 5.6µs | **1.44x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 11ns / 712ns | **63.96x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 11ns / 708ns | **63.64x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 255ns / 232ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 21ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.9µs / 8.1µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 18.0µs / 18.6µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | -| **Unique** | 6.3µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.6µs / 6.3µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.4µs / 3.2µs | **2.28x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.0µs / 3.0µs | **2.96x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **All** | 255ns / 238ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 254ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 141ns / 1.1µs | **7.84x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 253ns / 239ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.3µs / 8.6µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.3µs / 8.3µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 19.8µs / 44.9µs | **2.27x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 258ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 663ns / 1.0µs | **1.55x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **First** | 12ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **FirstWhere** | 257ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 8.2µs / 9.0µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 254ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.1µs / 10.8µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **Last** | 12ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Map** | 366ns / 800ns | **2.18x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Max** | 258ns / 235ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 256ns / 233ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 256ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 724ns / 1.3µs | **1.74x** | 48B / 12.3KB | **256.00x less** | 2 / 2 | +| **Reduce (sum)** | 259ns / 238ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 232ns / 239ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 4.0µs / 5.7µs | **1.43x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 11ns / 716ns | **62.99x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 12ns / 716ns | **62.29x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 258ns / 233ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 23ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.7µs / 7.9µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 17.5µs / 18.0µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Unique** | 6.5µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **UniqueBy** | 6.8µs / 6.5µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.4µs / 3.3µs | **2.27x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.0µs / 3.3µs | **3.22x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | + +Raw results for `collection.New().Clone()` (explicit copy) vs `lo`. + +| Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | +|---:|----------------|:--:|------------------|:--:|--------------------| +| **All** | 916ns / 239ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Any** | 906ns / 240ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Chunk** | 846ns / 1.1µs | **1.27x** | 9.5KB / 9.3KB | ≈ | 4 / 51 | +| **Contains** | 905ns / 241ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **CountBy** | 9.1µs / 8.5µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | +| **CountByValue** | 9.1µs / 8.5µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | +| **Difference** | 20.8µs / 44.9µs | **2.16x** | 98.6KB / 108.8KB | **1.10x less** | 18 / 43 | +| **Each** | 900ns / 233ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Filter** | 1.2µs / 1.0µs | 0.90x | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **First** | 738ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **FirstWhere** | 913ns / 241ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **GroupBySlice** | 8.9µs / 9.2µs | ≈ | 29.3KB / 21.0KB | 0.72x more | 86 / 83 | +| **IndexWhere** | 898ns / 240ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Intersect** | 12.3µs / 11.1µs | ≈ | 27.9KB / 11.4KB | 0.41x more | 26 / 19 | +| **Last** | 726ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Map** | 920ns / 803ns | 0.87x | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **Max** | 924ns / 233ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **Min** | 918ns / 232ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **None** | 901ns / 239ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Pipeline F→M→T→R** | 1.2µs / 1.3µs | ≈ | 8.3KB / 12.3KB | **1.49x less** | 4 / 2 | +| **Reduce (sum)** | 899ns / 238ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Reverse** | 840ns / 239ns | 0.28x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Shuffle** | 4.5µs / 5.7µs | **1.26x** | 16.5KB / 0B | ∞x more | 5 / 0 | +| **Skip** | 720ns / 725ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **SkipLast** | 742ns / 729ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **Sum** | 917ns / 234ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **Take** | 738ns / <1ns | 0.00x | 8.3KB / 0B | ∞x more | 4 / 0 | +| **ToMap** | 8.2µs / 8.0µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 8 / 6 | +| **Union** | 18.7µs / 18.1µs | ≈ | 106.8KB / 90.3KB | 0.85x more | 17 / 10 | +| **Unique** | 7.1µs / 6.4µs | 0.89x | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | +| **UniqueBy** | 7.4µs / 6.5µs | 0.87x | 53.4KB / 45.1KB | 0.85x more | 10 / 6 | +| **Zip** | 2.8µs / 3.3µs | **1.19x** | 32.9KB / 16.4KB | 0.50x more | 7 / 1 | +| **ZipWith** | 2.4µs / 3.3µs | **1.37x** | 24.7KB / 8.2KB | 0.33x more | 7 / 1 | diff --git a/README.md b/README.md index f1b2860..0908eb3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Latest tag - Tests + Tests Go Report Card

@@ -34,7 +34,7 @@ - **Map / Filter / Reduce** - clean functional transforms - **First / Last / FirstWhere / IndexWhere / Contains** helpers - **Sort, GroupBy, Chunk**, and more -- **Safe-by-default** - defensive copies where appropriate +- **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 @@ -93,6 +93,8 @@ That design choice doesn't matter much for some single operations. It matters a Full raw tables: see `BENCHMARKS.md`. +### Borrow-by-default (New) + #### Read-only scalar ops (wrapper overhead only) | Op | Speed vs lo | Memory | Allocs | @@ -115,17 +117,17 @@ Full raw tables: see `BENCHMARKS.md`. | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Chunk** | **7.39x** | -8.0KB | -49 | +| **Chunk** | **7.84x** | -8.0KB | -49 | | **Take** | ≈ | +48B | +2 | -| **Skip** | **63.96x** | -8.2KB | ≈ | -| **SkipLast** | **63.64x** | -8.2KB | ≈ | -| **Zip** | **2.28x** | +48B | +2 | -| **ZipWith** | **2.96x** | +48B | +2 | +| **Skip** | **62.99x** | -8.2KB | ≈ | +| **SkipLast** | **62.29x** | -8.2KB | ≈ | +| **Zip** | **2.27x** | +48B | +2 | +| **ZipWith** | **3.22x** | +48B | +2 | | **Unique** | ≈ | +24B | +1 | | **UniqueBy** | ≈ | +48B | +2 | | **Union** | ≈ | +72B | +3 | | **Intersect** | ≈ | +72B | +3 | -| **Difference** | **2.37x** | -51.9KB | -41 | +| **Difference** | **2.27x** | -26.7KB | -29 | | **GroupBySlice** | ≈ | +24B | +1 | | **CountBy** | ≈ | +24B | +1 | | **CountByValue** | ≈ | +24B | +1 | @@ -135,16 +137,72 @@ Full raw tables: see `BENCHMARKS.md`. | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Pipeline F→M→T→R** | **2.47x** | -12.2KB | ≈ | +| **Pipeline F→M→T→R** | **1.74x** | -12.2KB | ≈ | #### Mutating ops | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Map** | **2.68x** | -8.2KB | ≈ | -| **Filter** | **1.41x** | -8.2KB | ≈ | +| **Map** | **2.18x** | -8.2KB | ≈ | +| **Filter** | **1.55x** | -8.2KB | ≈ | | **Reverse** | ≈ | +24B | +1 | -| **Shuffle** | **1.44x** | +8.2KB | +3 | +| **Shuffle** | **1.43x** | +8.2KB | +3 | + + +### Explicit copy cost (Clone) + +#### Read-only scalar ops (wrapper overhead only) + +| Op | Speed vs lo | Memory | Allocs | +|---:|:-----------:|:------:|:------:| +| **All** | 0.26x | +8.2KB | +3 | +| **Any** | 0.27x | +8.2KB | +3 | +| **None** | 0.27x | +8.2KB | +3 | +| **First** | 0.00x | +8.2KB | +3 | +| **Last** | 0.00x | +8.2KB | +3 | +| **FirstWhere** | 0.26x | +8.2KB | +3 | +| **IndexWhere** | 0.27x | +8.2KB | +3 | +| **Contains** | 0.27x | +8.2KB | +3 | +| **Reduce (sum)** | 0.26x | +8.2KB | +3 | +| **Sum** | 0.25x | +8.3KB | +5 | +| **Min** | 0.25x | +8.3KB | +5 | +| **Max** | 0.25x | +8.3KB | +5 | +| **Each** | 0.26x | +8.2KB | +3 | + +#### Transforming ops + +| Op | Speed vs lo | Memory | Allocs | +|---:|:-----------:|:------:|:------:| +| **Chunk** | **1.27x** | +240B | -47 | +| **Take** | 0.00x | +8.3KB | +4 | +| **Skip** | ≈ | +48B | +2 | +| **SkipLast** | ≈ | +48B | +2 | +| **Zip** | **1.19x** | +16.5KB | +6 | +| **ZipWith** | **1.37x** | +16.5KB | +6 | +| **Unique** | 0.89x | +8.2KB | +3 | +| **UniqueBy** | 0.87x | +8.3KB | +4 | +| **Union** | ≈ | +16.5KB | +7 | +| **Intersect** | ≈ | +16.5KB | +7 | +| **Difference** | **2.16x** | -10.2KB | -25 | +| **GroupBySlice** | ≈ | +8.2KB | +3 | +| **CountBy** | ≈ | +8.2KB | +3 | +| **CountByValue** | ≈ | +8.2KB | +3 | +| **ToMap** | ≈ | +8.2KB | +2 | + +#### Pipelines + +| Op | Speed vs lo | Memory | Allocs | +|---:|:-----------:|:------:|:------:| +| **Pipeline F→M→T→R** | ≈ | -4.0KB | +2 | + +#### Mutating ops + +| Op | Speed vs lo | Memory | Allocs | +|---:|:-----------:|:------:|:------:| +| **Map** | 0.87x | +48B | +2 | +| **Filter** | 0.90x | +48B | +2 | +| **Reverse** | 0.28x | +8.2KB | +3 | +| **Shuffle** | **1.26x** | +16.5KB | +5 | ## How to read the benchmarks @@ -196,7 +254,8 @@ In these cases, `collection` can be **2×–30× faster** and often reduce alloc Fluent pipelines don't mean you're locked into mutation. -`New` borrows the input slice by default. If you need an owned copy, use `CopyOf()` or `Clone()` explicitly. +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. @@ -281,7 +340,7 @@ go get github.com/goforj/collection |------:|-----------| | **Access** | [Items](#items) [ItemsCopy](#itemscopy) | | **Aggregation** | [Avg](#avg) [Count](#count) [CountBy](#countby) [CountByValue](#countbyvalue) [Max](#max) [MaxBy](#maxby) [Median](#median) [Min](#min) [MinBy](#minby) [Mode](#mode) [Reduce](#reduce) [Sum](#sum) | -| **Construction** | [Clone](#clone) [CopyOf](#copyof) [CopyOfNumeric](#copyofnumeric) [New](#new) [NewNumeric](#newnumeric) | +| **Construction** | [Clone](#clone) [New](#new) [NewNumeric](#newnumeric) | | **Debugging** | [Dd](#dd) [Dump](#dump) [DumpStr](#dumpstr) | | **Grouping** | [GroupBy](#groupby) [GroupBySlice](#groupbyslice) | | **Maps** | [FromMap](#frommap) [ToMap](#tomap) [ToMapKV](#tomapkv) | @@ -880,7 +939,7 @@ collection.Dump(total3) ## Construction -### Clone · immutable · fluent +### Clone · immutable · chainable Clone returns a copy of the collection. @@ -944,45 +1003,11 @@ collection.Dump(odds.Items()) // ] ``` -### CopyOf · immutable · fluent - -CopyOf creates a new Collection by copying the provided slice. - -```go -items := []int{1, 2, 3} -c := collection.CopyOf(items) - -items[0] = 9 -collection.Dump(c.Items()) -// #[]int [ -// 0 => 1 #int -// 1 => 2 #int -// 2 => 3 #int -// ] -``` - -### CopyOfNumeric · immutable · fluent - -CopyOfNumeric creates a new NumericCollection by copying the provided slice. - -```go -items := []int{1, 2, 3} -c := collection.CopyOfNumeric(items) - -items[0] = 9 -collection.Dump(c.Items()) -// #[]int [ -// 0 => 1 #int -// 1 => 2 #int -// 2 => 3 #int -// ] -``` - -### New · immutable · fluent +### New · immutable · chainable New creates a new Collection from the provided slice and borrows it. -### NewNumeric · immutable · fluent +### NewNumeric · immutable · chainable NewNumeric wraps a slice of numeric types in a NumericCollection and borrows it. @@ -1004,7 +1029,7 @@ c.Dd() // Process finished with the exit code 1 ``` -### Dump · readonly · fluent +### 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. @@ -1209,7 +1234,7 @@ collection.Dump(groups2["user"]) ## Maps -### FromMap · immutable · fluent +### FromMap · immutable · chainable FromMap materializes a map into a collection of key/value pairs. @@ -1378,7 +1403,7 @@ collection.Dump(out2) ## Ordering -### After · immutable · fluent +### 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. @@ -1392,7 +1417,7 @@ c.After(func(v int) bool { return v == 3 }).Dump() // ] ``` -### Before · immutable · fluent +### Before · immutable · chainable Before returns a new collection containing all items that appear *before* the first element for which pred returns true. @@ -1445,7 +1470,7 @@ collection.Dump(out3.Items()) // ] ``` -### Reverse · mutable · fluent +### Reverse · mutable · chainable Reverse reverses the order of items in the collection in place and returns the same collection for chaining. @@ -1503,7 +1528,7 @@ collection.Dump(users.Items()) // ] ``` -### Shuffle · immutable · fluent +### Shuffle · immutable · chainable Shuffle returns a shuffled copy of the collection. @@ -1544,7 +1569,7 @@ out3 := users.Shuffle() collection.Dump(out3.Items()) ``` -### Sort · mutable · fluent +### Sort · mutable · chainable Sort sorts the collection in place using the provided comparison function and returns the same collection for chaining. @@ -2121,7 +2146,7 @@ fmt.Println(out1) ## Set Operations -### Difference · immutable · fluent +### 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 @@ -2187,7 +2212,7 @@ collection.Dump(out3.Items()) // ] ``` -### Intersect · immutable · fluent +### Intersect · immutable · chainable Intersect returns a new collection containing elements from the second collection that are also present in the first. @@ -2256,7 +2281,7 @@ collection.Dump(out3.Items()) // ] ``` -### SymmetricDifference · immutable · fluent +### 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 @@ -2324,7 +2349,7 @@ collection.Dump(out3.Items()) // ] ``` -### Union · immutable · fluent +### 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 @@ -2398,7 +2423,7 @@ collection.Dump(out3.Items()) // ] ``` -### Unique · immutable · fluent +### 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, @@ -2458,7 +2483,7 @@ collection.Dump(out3.Items()) // ] ``` -### UniqueBy · immutable · fluent +### UniqueBy · immutable · chainable UniqueBy returns a new collection containing only the first occurrence of each element as determined by keyFn. @@ -2515,7 +2540,7 @@ collection.Dump(out3.Items()) // ] ``` -### UniqueComparable · immutable · fluent +### 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. @@ -2620,7 +2645,7 @@ collection.Dump(userChunks) //] ``` -### Filter · mutable · fluent +### 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. @@ -2685,7 +2710,7 @@ collection.Dump(users.Items()) // ] ``` -### Partition · immutable · fluent +### Partition · immutable · chainable Partition splits the collection into two new collections based on predicate fn. The first collection contains items where fn returns true; the second contains @@ -2934,7 +2959,7 @@ collection.Dump(popped5, c4.Items()) // ] ``` -### Skip · immutable · fluent +### 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. @@ -3001,7 +3026,7 @@ collection.Dump(out4.Items()) // ] ``` -### SkipLast · immutable · fluent +### 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. @@ -3064,7 +3089,7 @@ collection.Dump(out4.Items()) // ] ``` -### Take · immutable · fluent +### Take · immutable · chainable Take returns a new collection containing the first `n` items when n > 0, or the last `|n|` items when n < 0. @@ -3116,7 +3141,7 @@ collection.Dump(out4.Items()) // ] ``` -### TakeLast · immutable · fluent +### 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. @@ -3177,7 +3202,7 @@ collection.Dump(out4.Items()) // ] ``` -### TakeUntil · immutable · fluent +### TakeUntil · immutable · chainable TakeUntil returns items until the first element equals `value`. The matching item is NOT included. @@ -3217,7 +3242,7 @@ collection.Dump(out6.Items()) // ] ``` -### TakeUntilFn · immutable · fluent +### TakeUntilFn · immutable · chainable TakeUntilFn returns items until the predicate function returns true. The matching item is NOT included. @@ -3257,7 +3282,7 @@ collection.Dump(out3.Items()) // ] ``` -### Window · allocates · fluent +### 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). @@ -3359,7 +3384,7 @@ collection.Dump(win3.Items()) ## Transformation -### Append · immutable · fluent +### Append · immutable · chainable Append returns a new collection with the given values appended. @@ -3414,7 +3439,7 @@ users.Append( // ] ``` -### Concat · mutable · fluent +### Concat · mutable · chainable Concat appends the values from the given slice onto the end of the collection, @@ -3433,7 +3458,7 @@ collection.Dump(concatenated) // ] ``` -### Each · readonly · fluent +### 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.). @@ -3497,9 +3522,9 @@ collection.Dump(names) // ] ``` -### Map · mutable · fluent +### Map · mutable · chainable -Map applies a same-type transformation in place. +Map applies a same-type transformation in place and returns the same collection. _Example: integers_ @@ -3566,7 +3591,7 @@ collection.Dump(updated.Items()) // ] ``` -### MapTo · immutable · fluent +### MapTo · immutable · chainable MapTo maps a Collection[T] to a Collection[R] using fn(T) R. @@ -3628,7 +3653,7 @@ collection.Dump(names.Items()) // ] ``` -### Merge · immutable · fluent +### Merge · immutable · chainable Merge merges the given data into a new collection. @@ -3704,7 +3729,7 @@ collection.Dump(merged3.Items()) // ] ``` -### Multiply · immutable · fluent +### Multiply · immutable · chainable Multiply creates `n` copies of all items in the collection and returns a new collection. @@ -3837,7 +3862,7 @@ collection.Dump(names) // ] ``` -### Prepend · mutable · fluent +### Prepend · mutable · chainable Prepend adds the given values to the beginning of the collection. @@ -3919,7 +3944,7 @@ collection.Dump(c2.Items()) // ] ``` -### Tap · immutable · fluent +### Tap · immutable · chainable Tap invokes fn with the collection pointer for side effects (logging, debugging, inspection) and returns the same collection to allow chaining. @@ -3979,7 +4004,7 @@ users2 := users.Tap(func(col *collection.Collection[User]) { collection.Dump(users2.Items()) // ensures users2 is used ``` -### Times · immutable · fluent +### 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. @@ -4101,7 +4126,7 @@ collection.Dump(c3.Items()) // ] ``` -### Zip · immutable · fluent +### Zip · immutable · chainable Zip combines two collections element-wise into a collection of tuples. The resulting length is the smaller of the two inputs. @@ -4161,7 +4186,7 @@ collection.Dump(out2.Items()) // ] ``` -### ZipWith · immutable · fluent +### ZipWith · immutable · chainable ZipWith combines two collections element-wise using combiner fn. The resulting length is the smaller of the two inputs. diff --git a/after.go b/after.go index a5aba36..7be389b 100644 --- a/after.go +++ b/after.go @@ -4,7 +4,8 @@ package collection // If no element matches, an empty collection is returned. // @group Ordering // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // NOTE: returns a view (shares backing array). Use Clone() to detach. // diff --git a/all.go b/all.go index 9801511..720cbf2 100644 --- a/all.go +++ b/all.go @@ -4,7 +4,7 @@ package collection // If the collection is empty, All returns true (vacuously true). // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers – all even diff --git a/any.go b/any.go index 1586ca7..3e7e5ec 100644 --- a/any.go +++ b/any.go @@ -3,7 +3,7 @@ package collection // Any returns true if at least one item satisfies fn. // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // Example: integers // diff --git a/append.go b/append.go index 957e63e..8102269 100644 --- a/append.go +++ b/append.go @@ -3,7 +3,8 @@ package collection // Append returns a new collection with the given values appended. // @group Transformation // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // Example: integers // // c := collection.New([]int{1, 2}) diff --git a/at.go b/at.go index 9d969cf..b5064d3 100644 --- a/at.go +++ b/at.go @@ -4,7 +4,7 @@ package collection // whether the index was within bounds. // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // This method is safe and does not panic for out-of-range indices. diff --git a/avg.go b/avg.go index 67f7697..b02860d 100644 --- a/avg.go +++ b/avg.go @@ -4,7 +4,7 @@ package collection // If the collection is empty, Avg returns 0. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers diff --git a/before.go b/before.go index 9d44d32..ebc865b 100644 --- a/before.go +++ b/before.go @@ -4,7 +4,8 @@ package collection // *before* the first element for which pred returns true. // @group Ordering // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // NOTE: returns a view (shares backing array). Use Clone() to detach. // diff --git a/chunk.go b/chunk.go index d158529..48a2689 100644 --- a/chunk.go +++ b/chunk.go @@ -4,7 +4,7 @@ package collection // The final chunk may be smaller if len(items) is not divisible by size. // @group Slicing // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // If size <= 0, nil is returned. diff --git a/clone.go b/clone.go index 69cea1b..cdcaa60 100644 --- a/clone.go +++ b/clone.go @@ -1,7 +1,8 @@ package collection // Clone returns a copy of the collection. -// @fluent true +// @chainable true +// @terminal false // // The returned collection has its own backing slice, so subsequent mutations // do not affect the original collection. diff --git a/collection.go b/collection.go index fa78541..eb2fcda 100644 --- a/collection.go +++ b/collection.go @@ -24,7 +24,8 @@ type Pair[K comparable, V any] struct { // New creates a new Collection from the provided slice and borrows it. // @group Construction // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // The returned Collection is a lightweight, strongly-typed wrapper // around the slice, enabling fluent, chainable operations such as @@ -41,77 +42,18 @@ type NumericCollection[T Number] struct { // NewNumeric wraps a slice of numeric types in a NumericCollection and borrows it. // @group Construction // @behavior immutable -// @fluent true +// @chainable true +// @terminal false func NewNumeric[T Number](items []T) *NumericCollection[T] { return &NumericCollection[T]{ Collection: &Collection[T]{items: items}, } } -// CopyOf creates a new Collection by copying the provided slice. -// @group Construction -// @behavior immutable -// @fluent true -// -// CopyOf allocates a new backing slice. -// -// Example: copying input slice -// -// items := []int{1, 2, 3} -// c := collection.CopyOf(items) -// -// items[0] = 9 -// collection.Dump(c.Items()) -// // #[]int [ -// // 0 => 1 #int -// // 1 => 2 #int -// // 2 => 3 #int -// // ] -func CopyOf[T any](items []T) *Collection[T] { - var out []T - if items == nil { - out = nil - } else { - out = make([]T, len(items)) - copy(out, items) - } - return &Collection[T]{items: out} -} - -// CopyOfNumeric creates a new NumericCollection by copying the provided slice. -// @group Construction -// @behavior immutable -// @fluent true -// -// CopyOfNumeric allocates a new backing slice. -// -// Example: copying input slice -// -// items := []int{1, 2, 3} -// c := collection.CopyOfNumeric(items) -// -// items[0] = 9 -// collection.Dump(c.Items()) -// // #[]int [ -// // 0 => 1 #int -// // 1 => 2 #int -// // 2 => 3 #int -// // ] -func CopyOfNumeric[T Number](items []T) *NumericCollection[T] { - var out []T - if items == nil { - out = nil - } else { - out = make([]T, len(items)) - copy(out, items) - } - return &NumericCollection[T]{Collection: &Collection[T]{items: out}} -} - // Items returns the backing slice of items. // @group Access // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Items shares the backing array with the collection. Mutating the returned @@ -169,7 +111,7 @@ func (c *Collection[T]) Items() []T { // ItemsCopy returns a copy of the collection's items. // @group Access // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // ItemsCopy allocates a new slice. diff --git a/concat.go b/concat.go index ce9f8fd..9ca186b 100644 --- a/concat.go +++ b/concat.go @@ -3,7 +3,8 @@ package collection // Concat appends the values from the given slice onto the end of the collection, // @group Transformation // @behavior mutable -// @fluent true +// @chainable true +// @terminal false // // Example: strings // diff --git a/constructor_test.go b/constructor_test.go index 9b0d467..0761105 100644 --- a/constructor_test.go +++ b/constructor_test.go @@ -22,26 +22,6 @@ func TestNew_PreservesNilSlice(t *testing.T) { } } -func TestCopyOf_CopiesInputSlice(t *testing.T) { - items := []int{1, 2, 3} - c := CopyOf(items) - - items[0] = 9 - - if c.Items()[0] == 9 { - t.Fatalf("CopyOf should copy input slice") - } -} - -func TestCopyOf_PreservesNilSlice(t *testing.T) { - var items []int - c := CopyOf(items) - - if c.Items() != nil { - t.Fatalf("CopyOf should preserve nil slice") - } -} - func TestNewNumeric_PreservesNilSlice(t *testing.T) { var items []int c := NewNumeric(items) @@ -62,26 +42,6 @@ func TestNewNumeric_BorrowsInputSlice(t *testing.T) { } } -func TestCopyOfNumeric_CopiesInputSlice(t *testing.T) { - items := []int{1, 2, 3} - c := CopyOfNumeric(items) - - items[0] = 9 - - if c.Items()[0] == 9 { - t.Fatalf("CopyOfNumeric should copy input slice") - } -} - -func TestCopyOfNumeric_PreservesNilSlice(t *testing.T) { - var items []int - c := CopyOfNumeric(items) - - if c.Items() != nil { - t.Fatalf("CopyOfNumeric should preserve nil slice") - } -} - func TestSelectionOps_ShareBackingSlice(t *testing.T) { items := []int{1, 2, 3, 4} c := New(items) diff --git a/contains.go b/contains.go index d800d8f..3340c78 100644 --- a/contains.go +++ b/contains.go @@ -3,7 +3,7 @@ package collection // Contains returns true if the collection contains the given value. // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Similar to: Any (differs by using equality on a value). diff --git a/count.go b/count.go index e840be7..4d08484 100644 --- a/count.go +++ b/count.go @@ -3,7 +3,7 @@ package collection // Count returns the total number of items in the collection. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // Example: integers // diff --git a/count_by.go b/count_by.go index adc9b2e..d282f3a 100644 --- a/count_by.go +++ b/count_by.go @@ -4,7 +4,7 @@ package collection // K must be comparable. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers @@ -70,7 +70,7 @@ func CountBy[T any, K comparable](c *Collection[T], fn func(T) K) map[K]int { // is mapped to the number of times it appears. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // T must be comparable. diff --git a/difference.go b/difference.go index 9815e67..d59a3f9 100644 --- a/difference.go +++ b/difference.go @@ -5,7 +5,8 @@ package collection // duplicates are removed. // @group Set Operations // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Example: integers // diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..6e0f8fd --- /dev/null +++ b/doc.go @@ -0,0 +1,5 @@ +// Package collection provides fluent, explicit pipelines over slices. +// +// This library borrows slices by default. It does not perform defensive copies. +// Use Clone() or ItemsCopy() to explicitly copy. +package collection diff --git a/docs/backfill/main.go b/docs/backfill/main.go index 6286db4..09b1cb7 100644 --- a/docs/backfill/main.go +++ b/docs/backfill/main.go @@ -19,7 +19,7 @@ func main() { fmt.Println("Error:", err) os.Exit(1) } - fmt.Println("✔ @behavior and @fluent annotations backfilled") + fmt.Println("✔ @behavior, @chainable, and @terminal annotations backfilled") } func run() error { @@ -56,10 +56,12 @@ func run() error { continue } - // Fluent backfill (boolean) – always refresh to keep canonical. + // Chainable/terminal backfill (boolean) – always refresh to keep canonical. stripChainable(fn.Doc) // legacy cleanup stripFluent(fn.Doc) - insertFluent(fn.Doc, inferFluent(fn)) + stripTerminal(fn.Doc) + insertChainable(fn.Doc, inferChainable(fn)) + insertTerminal(fn.Doc, inferTerminal(fn)) changed = true // Respect explicit annotation @@ -116,21 +118,25 @@ func inferBehavior(fn *ast.FuncDecl) (string, bool) { return "", false } -func inferFluent(fn *ast.FuncDecl) string { - if knownFluentTrue[fn.Name.Name] { - return "true" +func inferChainable(fn *ast.FuncDecl) string { + if fn.Type.Results == nil || len(fn.Type.Results.List) != 1 { + return "false" } - if isCollectionMethod(fn) { + if returnsCollectionLike(fn) { return "true" } - if returnsCollectionLike(fn) && len(fn.Type.Results.List) == 1 { - return "true" - } return "false" } +func inferTerminal(fn *ast.FuncDecl) string { + if inferChainable(fn) == "true" { + return "false" + } + return "true" +} + var knownMutators = map[string]bool{ "Push": true, "Pop": true, @@ -145,33 +151,6 @@ var knownMutators = map[string]bool{ "Transform": true, } -var knownFluentTrue = map[string]bool{ - "Dd": true, -} - -func isCollectionMethod(fn *ast.FuncDecl) bool { - if fn.Recv == nil || len(fn.Recv.List) == 0 { - return false - } - - typ := fn.Recv.List[0].Type - star, ok := typ.(*ast.StarExpr) - if !ok { - return false - } - - switch t := star.X.(type) { - case *ast.IndexExpr: - if ident, ok := t.X.(*ast.Ident); ok { - return ident.Name == "Collection" - } - case *ast.Ident: - return t.Name == "NumericCollection" - } - - return false -} - func returnsCollection(fn *ast.FuncDecl) bool { if fn.Type.Results == nil || len(fn.Type.Results.List) != 1 { return false @@ -309,23 +288,33 @@ func hasBehavior(doc *ast.CommentGroup) bool { return false } -func hasFluent(doc *ast.CommentGroup) bool { - for _, c := range doc.List { - if strings.Contains(c.Text, "@fluent") { - return true +func insertBehavior(doc *ast.CommentGroup, behavior string) { + for i, c := range doc.List { + text := strings.TrimSpace(strings.TrimPrefix(c.Text, "//")) + if strings.HasPrefix(text, "Example:") || text == "" { + doc.List = append( + doc.List[:i], + append([]*ast.Comment{ + {Text: fmt.Sprintf("// @behavior %s", behavior)}, + }, doc.List[i:]...)..., + ) + return } } - return false + + doc.List = append(doc.List, &ast.Comment{ + Text: fmt.Sprintf("// @behavior %s", behavior), + }) } -func insertBehavior(doc *ast.CommentGroup, behavior string) { +func insertChainable(doc *ast.CommentGroup, val string) { for i, c := range doc.List { text := strings.TrimSpace(strings.TrimPrefix(c.Text, "//")) if strings.HasPrefix(text, "Example:") || text == "" { doc.List = append( doc.List[:i], append([]*ast.Comment{ - {Text: fmt.Sprintf("// @behavior %s", behavior)}, + {Text: fmt.Sprintf("// @chainable %s", val)}, }, doc.List[i:]...)..., ) return @@ -333,18 +322,18 @@ func insertBehavior(doc *ast.CommentGroup, behavior string) { } doc.List = append(doc.List, &ast.Comment{ - Text: fmt.Sprintf("// @behavior %s", behavior), + Text: fmt.Sprintf("// @chainable %s", val), }) } -func insertFluent(doc *ast.CommentGroup, val string) { +func insertTerminal(doc *ast.CommentGroup, val string) { for i, c := range doc.List { text := strings.TrimSpace(strings.TrimPrefix(c.Text, "//")) if strings.HasPrefix(text, "Example:") || text == "" { doc.List = append( doc.List[:i], append([]*ast.Comment{ - {Text: fmt.Sprintf("// @fluent %s", val)}, + {Text: fmt.Sprintf("// @terminal %s", val)}, }, doc.List[i:]...)..., ) return @@ -352,7 +341,7 @@ func insertFluent(doc *ast.CommentGroup, val string) { } doc.List = append(doc.List, &ast.Comment{ - Text: fmt.Sprintf("// @fluent %s", val), + Text: fmt.Sprintf("// @terminal %s", val), }) } @@ -378,6 +367,17 @@ func stripFluent(doc *ast.CommentGroup) { doc.List = out } +func stripTerminal(doc *ast.CommentGroup) { + out := doc.List[:0] + for _, c := range doc.List { + if strings.Contains(c.Text, "@terminal") { + continue + } + out = append(out, c) + } + doc.List = out +} + // // ------------------------------------------------------------ // File IO diff --git a/docs/bench/main.go b/docs/bench/main.go index c30312c..ba649d3 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -39,14 +39,16 @@ func main() { start := time.Now() only := parseOnly(*onlyFlag) borrowResults := runBenches(only, benchBorrow) - condensed := renderCondensedTables(borrowResults) - rawTable := renderTable(borrowResults) + copyResults := runBenches(only, benchCopy) + condensed := renderCondensedTables(borrowResults, copyResults) + rawBorrow := renderTable(borrowResults) + rawCopy := renderTable(copyResults) if err := updateReadme(condensed); err != nil { fmt.Println("Error:", err) os.Exit(1) } - if err := updateBenchmarksFile(rawTable); err != nil { + if err := updateBenchmarksFile(rawBorrow, rawCopy); err != nil { fmt.Println("Error:", err) os.Exit(1) } @@ -65,6 +67,7 @@ type benchMode string const ( benchBorrow benchMode = "borrow" + benchCopy benchMode = "copy" ) var ( @@ -76,6 +79,14 @@ var ( func setBenchMode(mode benchMode) { currentMode = mode switch mode { + case benchCopy: + ctorInts = func(items []int) *collection.Collection[int] { + return collection.New(items).Clone() + } + ctorNumericInt = func(items []int) *collection.NumericCollection[int] { + base := collection.NewNumeric(items) + return &collection.NumericCollection[int]{Collection: base.Collection.Clone()} + } default: ctorInts = collection.New[int] ctorNumericInt = collection.NewNumeric[int] @@ -402,7 +413,7 @@ func benchMapCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < benchInner; j++ { - input := benchInts + input := collectionInputForMutating(benchInts) _ = ctorInts(input).Map(func(v int) int { return v * 3 }) } @@ -972,15 +983,7 @@ type benchGroup struct { ops []string } -func renderCondensedTables(results []benchResult) string { - byName := map[string]map[string]benchResult{} - for _, r := range results { - if _, ok := byName[r.name]; !ok { - byName[r.name] = map[string]benchResult{} - } - byName[r.name][r.impl] = r - } - +func renderCondensedTables(borrowResults, copyResults []benchResult) string { groups := []benchGroup{ { name: "Read-only scalar ops (wrapper overhead only)", @@ -1039,7 +1042,22 @@ func renderCondensedTables(results []benchResult) string { var buf bytes.Buffer buf.WriteString("Full raw tables: see `BENCHMARKS.md`.\n\n") + renderCondensedTable(&buf, "Borrow-by-default (New)", borrowResults, groups) + buf.WriteString("\n") + renderCondensedTable(&buf, "Explicit copy cost (Clone)", copyResults, groups) + return strings.TrimSpace(buf.String()) +} + +func renderCondensedTable(buf *bytes.Buffer, title string, results []benchResult, groups []benchGroup) { + byName := map[string]map[string]benchResult{} + for _, r := range results { + if _, ok := byName[r.name]; !ok { + byName[r.name] = map[string]benchResult{} + } + byName[r.name][r.impl] = r + } + buf.WriteString(fmt.Sprintf("### %s\n\n", title)) for _, group := range groups { rows := make([]string, 0, len(group.ops)) for _, name := range group.ops { @@ -1069,8 +1087,6 @@ func renderCondensedTables(results []benchResult) string { buf.WriteString(strings.Join(rows, "\n")) buf.WriteString("\n\n") } - - return strings.TrimSpace(buf.String()) } func formatNs(ns float64) string { @@ -1279,7 +1295,7 @@ func replaceBenchTable(section, condensed string) (string, error) { return "\n\n" + trimmed + "\n", nil } -func updateBenchmarksFile(rawTable string) error { +func updateBenchmarksFile(rawBorrowTable, rawCopyTable string) error { root, err := findRoot() if err != nil { return err @@ -1289,7 +1305,10 @@ func updateBenchmarksFile(rawTable string) error { var buf bytes.Buffer buf.WriteString("# Benchmarks\n\n") buf.WriteString("Raw results for `collection.New` (borrowed) vs `lo`.\n\n") - buf.WriteString(rawTable) + buf.WriteString(rawBorrowTable) + buf.WriteString("\n\n") + buf.WriteString("Raw results for `collection.New().Clone()` (explicit copy) vs `lo`.\n\n") + buf.WriteString(rawCopyTable) buf.WriteString("\n") return os.WriteFile(path, buf.Bytes(), 0o644) } diff --git a/docs/readme/main.go b/docs/readme/main.go index 1c7c63f..04857ad 100644 --- a/docs/readme/main.go +++ b/docs/readme/main.go @@ -80,7 +80,7 @@ type FuncDoc struct { Name string Group string Behavior string - Fluent string + Chainable string Terminal string Description string Examples []Example @@ -101,7 +101,7 @@ type Example struct { var ( groupHeader = regexp.MustCompile(`(?i)^\s*@group\s+(.+)$`) behaviorHeader = regexp.MustCompile(`(?i)^\s*@behavior\s+(.+)$`) - fluentHeader = regexp.MustCompile(`(?i)^\s*@fluent\s+(.+)$`) + chainableHeader = regexp.MustCompile(`(?i)^\s*@chainable\s+(.+)$`) terminalHeader = regexp.MustCompile(`(?i)^\s*@terminal\s+(.+)$`) exampleHeader = regexp.MustCompile(`(?i)^\s*Example:\s*(.*)$`) ) @@ -148,7 +148,7 @@ func parseFuncs(root string) ([]*FuncDoc, error) { Name: fn.Name.Name, Group: extractGroup(fn.Doc), Behavior: extractBehavior(fn.Doc), - Fluent: extractFluent(fn.Doc), + Chainable: extractChainable(fn.Doc), Terminal: extractTerminal(fn.Doc), Description: extractDescription(fn.Doc), Examples: extractExamples(fset, fn), @@ -193,10 +193,10 @@ func extractBehavior(group *ast.CommentGroup) string { return "" } -func extractFluent(group *ast.CommentGroup) string { +func extractChainable(group *ast.CommentGroup) string { for _, c := range group.List { line := strings.TrimSpace(strings.TrimPrefix(c.Text, "//")) - if m := fluentHeader.FindStringSubmatch(line); m != nil { + if m := chainableHeader.FindStringSubmatch(line); m != nil { return strings.ToLower(strings.TrimSpace(m[1])) } } @@ -222,7 +222,7 @@ func extractDescription(group *ast.CommentGroup) string { if exampleHeader.MatchString(line) || groupHeader.MatchString(line) || behaviorHeader.MatchString(line) || - fluentHeader.MatchString(line) || + chainableHeader.MatchString(line) || terminalHeader.MatchString(line) { break } @@ -383,8 +383,8 @@ func renderAPI(funcs []*FuncDoc) string { if fn.Behavior != "" { header += " · " + fn.Behavior } - if fn.Fluent == "true" { - header += " · fluent" + if fn.Chainable == "true" { + header += " · chainable" } if fn.Terminal == "true" { header += " · terminal" diff --git a/dump.go b/dump.go index d258a4f..48a548d 100644 --- a/dump.go +++ b/dump.go @@ -13,7 +13,8 @@ var exitFunc = func(v interface{}) { godump.Dd(v) } // This is a no-op on the collection itself and never panics. // @group Debugging // @behavior readonly -// @fluent true +// @chainable true +// @terminal false // // Example: integers // @@ -43,7 +44,7 @@ func (c *Collection[T]) Dump() *Collection[T] { // Like Laravel's dd(), this is intended for debugging and // should not be used in production control flow. // @group Debugging -// @fluent false +// @chainable false // @terminal true // // This method never returns. @@ -66,7 +67,7 @@ func (c *Collection[T]) Dd() { // Useful for logging, snapshot testing, and non-interactive debugging. // @group Debugging // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers @@ -92,7 +93,7 @@ func setDumpWriter(w io.Writer) { // Dump is a convenience function that calls godump.Dump. // @group Debugging -// @fluent false +// @chainable false // @terminal true // // Example: integers diff --git a/each.go b/each.go index df439d3..fe5b7af 100644 --- a/each.go +++ b/each.go @@ -4,7 +4,8 @@ package collection // so it can be used in chains for side effects (logging, debugging, etc.). // @group Transformation // @behavior readonly -// @fluent true +// @chainable true +// @terminal false // // Example: integers // diff --git a/examples/clone/main.go b/examples/clone/main.go index e672100..e4321c6 100644 --- a/examples/clone/main.go +++ b/examples/clone/main.go @@ -7,7 +7,8 @@ import "github.com/goforj/collection" func main() { // Clone returns a copy of the collection. - // @fluent true + // @chainable true + // @terminal false // // The returned collection has its own backing slice, so subsequent mutations // do not affect the original collection. diff --git a/examples/copyof/main.go b/examples/copyof/main.go deleted file mode 100644 index 8f95e8c..0000000 --- a/examples/copyof/main.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import "github.com/goforj/collection" - -func main() { - // CopyOf creates a new Collection by copying the provided slice. - - // Example: copying input slice - items := []int{1, 2, 3} - c := collection.CopyOf(items) - - items[0] = 9 - collection.Dump(c.Items()) - // #[]int [ - // 0 => 1 #int - // 1 => 2 #int - // 2 => 3 #int - // ] -} diff --git a/examples/copyofnumeric/main.go b/examples/copyofnumeric/main.go deleted file mode 100644 index 8d0e615..0000000 --- a/examples/copyofnumeric/main.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import "github.com/goforj/collection" - -func main() { - // CopyOfNumeric creates a new NumericCollection by copying the provided slice. - - // Example: copying input slice - items := []int{1, 2, 3} - c := collection.CopyOfNumeric(items) - - items[0] = 9 - collection.Dump(c.Items()) - // #[]int [ - // 0 => 1 #int - // 1 => 2 #int - // 2 => 3 #int - // ] -} diff --git a/examples/map/main.go b/examples/map/main.go index 005a232..8bd8986 100644 --- a/examples/map/main.go +++ b/examples/map/main.go @@ -9,7 +9,7 @@ import ( ) func main() { - // Map applies a same-type transformation in place. + // Map applies a same-type transformation in place and returns the same collection. // Example: integers c := collection.New([]int{1, 2, 3}) diff --git a/examples/take/main.go b/examples/take/main.go index dd9511b..f9ed168 100644 --- a/examples/take/main.go +++ b/examples/take/main.go @@ -8,7 +8,8 @@ import "github.com/goforj/collection" func main() { // Take returns a new collection containing the first `n` items when n > 0, // or the last `|n|` items when n < 0. - // @fluent true + // @chainable true + // @terminal false // // If n exceeds the collection length, the entire collection is returned. // If n == 0, an empty collection is returned. diff --git a/examples/takelast/main.go b/examples/takelast/main.go index b298e65..ee4b812 100644 --- a/examples/takelast/main.go +++ b/examples/takelast/main.go @@ -10,7 +10,8 @@ func main() { // 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. - // @fluent true + // @chainable true + // @terminal false // // This operation performs no element allocations; it re-slices the // underlying slice. diff --git a/examples/takeuntil/main.go b/examples/takeuntil/main.go index f017c7c..27edd10 100644 --- a/examples/takeuntil/main.go +++ b/examples/takeuntil/main.go @@ -8,7 +8,8 @@ import "github.com/goforj/collection" func main() { // TakeUntil returns items until the first element equals `value`. // The matching item is NOT included. - // @fluent true + // @chainable true + // @terminal false // // Uses == comparison, so T must be comparable. diff --git a/filter.go b/filter.go index 11157b4..13caacc 100644 --- a/filter.go +++ b/filter.go @@ -4,7 +4,8 @@ package collection // This method mutates the collection in place and returns the same instance. // @group Slicing // @behavior mutable -// @fluent true +// @chainable true +// @terminal false // Example: integers // // c := collection.New([]int{1, 2, 3, 4}) diff --git a/first.go b/first.go index 4701e1d..31fb1a8 100644 --- a/first.go +++ b/first.go @@ -4,7 +4,7 @@ package collection // If the collection is empty, ok will be false. // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers diff --git a/first_where.go b/first_where.go index a1a8493..50f098a 100644 --- a/first_where.go +++ b/first_where.go @@ -5,7 +5,7 @@ package collection // along with the zero value of T. // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // This method is equivalent to Laravel's collection->first(fn) and mirrors diff --git a/from_map.go b/from_map.go index d99b934..a4dee8b 100644 --- a/from_map.go +++ b/from_map.go @@ -3,7 +3,8 @@ package collection // FromMap materializes a map into a collection of key/value pairs. // @group Maps // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // The iteration order of the resulting collection is unspecified, // matching Go's map iteration semantics. diff --git a/group_by.go b/group_by.go index 3aeee84..4e591a3 100644 --- a/group_by.go +++ b/group_by.go @@ -4,7 +4,7 @@ package collection // returned from keyFn. // @group Grouping // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // The order of items within each group is preserved. diff --git a/group_by_slice.go b/group_by_slice.go index 5166733..be8d5b4 100644 --- a/group_by_slice.go +++ b/group_by_slice.go @@ -4,7 +4,7 @@ package collection // returned from keyFn. // @group Grouping // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // The order of items within each group is preserved. diff --git a/index_where.go b/index_where.go index 801ab2a..36810f5 100644 --- a/index_where.go +++ b/index_where.go @@ -5,7 +5,7 @@ package collection // If no item matches, it returns (0, false). // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // This operation performs no allocations and short-circuits diff --git a/intersect.go b/intersect.go index c898cef..6fd8db9 100644 --- a/intersect.go +++ b/intersect.go @@ -4,7 +4,8 @@ package collection // collection that are also present in the first. // @group Set Operations // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Order follows the second collection. // Duplicates are preserved based on the second collection. diff --git a/is_empty.go b/is_empty.go index 0fda8a0..e17fa9a 100644 --- a/is_empty.go +++ b/is_empty.go @@ -3,7 +3,7 @@ package collection // IsEmpty returns true if the collection has no items. // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers (non-empty) diff --git a/last.go b/last.go index 5d19f33..25dbd36 100644 --- a/last.go +++ b/last.go @@ -4,7 +4,7 @@ package collection // If the collection is empty, ok will be false. // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers diff --git a/last_where.go b/last_where.go index 973b5ca..e9afcd2 100644 --- a/last_where.go +++ b/last_where.go @@ -5,7 +5,7 @@ package collection // If the collection is empty or no element matches, ok will be false. // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers diff --git a/map.go b/map.go index 37149d7..b540788 100644 --- a/map.go +++ b/map.go @@ -1,11 +1,13 @@ package collection -// Map applies a same-type transformation in place. +// Map applies a same-type transformation in place and returns the same collection. // @group Transformation // @behavior mutable -// @fluent true +// @chainable true +// @terminal false // // Use this when you're transforming T -> T (e.g., enrichment, normalization). +// Map mutates the receiver's backing slice. Use Clone() if you need isolation. // // Example: integers // diff --git a/max.go b/max.go index dc2d9f0..c982952 100644 --- a/max.go +++ b/max.go @@ -4,7 +4,7 @@ package collection // The second return value is false if the collection is empty. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers diff --git a/max_by.go b/max_by.go index 4c17202..562290c 100644 --- a/max_by.go +++ b/max_by.go @@ -4,7 +4,7 @@ package collection // The second return value is false if the collection is empty. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // This cannot be a method because methods can't introduce a new type parameter K. diff --git a/median.go b/median.go index 9ffbaf6..08c972b 100644 --- a/median.go +++ b/median.go @@ -6,7 +6,7 @@ import "sort" // Returns (0, false) if the collection is empty. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Odd count → middle value diff --git a/merge.go b/merge.go index 33d71fe..4c1b028 100644 --- a/merge.go +++ b/merge.go @@ -3,7 +3,8 @@ package collection // Merge merges the given data into a new collection. // @group Transformation // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Example: integers - merging slices // diff --git a/min.go b/min.go index 1122007..bc32c7b 100644 --- a/min.go +++ b/min.go @@ -4,7 +4,7 @@ package collection // The second return value is false if the collection is empty. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers diff --git a/min_by.go b/min_by.go index ae033ed..271f808 100644 --- a/min_by.go +++ b/min_by.go @@ -4,7 +4,7 @@ package collection // The second return value is false if the collection is empty. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // This cannot be a method because methods can't introduce a new type parameter K. diff --git a/mode.go b/mode.go index f23d4b8..e870db2 100644 --- a/mode.go +++ b/mode.go @@ -5,7 +5,7 @@ package collection // in first-seen order. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers – single mode diff --git a/multiply.go b/multiply.go index 3a455f2..b80f098 100644 --- a/multiply.go +++ b/multiply.go @@ -4,7 +4,8 @@ package collection // and returns a new collection. // @group Transformation // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Example: integers // diff --git a/none.go b/none.go index cca2793..ec398c5 100644 --- a/none.go +++ b/none.go @@ -4,7 +4,7 @@ package collection // If the collection is empty, None returns true. // @group Querying // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers – none even diff --git a/partition.go b/partition.go index 4107edb..2c92143 100644 --- a/partition.go +++ b/partition.go @@ -5,7 +5,8 @@ package collection // items where fn returns false. Order is preserved within each partition. // @group Slicing // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Example: integers - even/odd // diff --git a/pipe.go b/pipe.go index 023b6e6..acc0394 100644 --- a/pipe.go +++ b/pipe.go @@ -4,7 +4,7 @@ package collection // and returns the function's result. // @group Transformation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // This is a typed escape hatch for branching into arbitrary logic without `any`. diff --git a/pluck.go b/pluck.go index 79e0ce0..ddd25fa 100644 --- a/pluck.go +++ b/pluck.go @@ -3,7 +3,8 @@ package collection // MapTo maps a Collection[T] to a Collection[R] using fn(T) R. // @group Transformation // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // This cannot be a method because methods can't introduce a new type parameter R. // diff --git a/pop.go b/pop.go index 679a482..28df5c5 100644 --- a/pop.go +++ b/pop.go @@ -3,7 +3,7 @@ package collection // Pop removes and returns the last item in the collection. // @group Slicing // @behavior mutable -// @fluent false +// @chainable false // @terminal true // // If the collection is empty, the zero value of T is returned with ok=false. @@ -83,7 +83,7 @@ func (c *Collection[T]) Pop() (T, bool) { // PopN removes and returns the last n items in original order. // @group Slicing // @behavior mutable -// @fluent false +// @chainable false // @terminal true // // Example: integers – pop 2 diff --git a/prepend.go b/prepend.go index 5338fc5..408d151 100644 --- a/prepend.go +++ b/prepend.go @@ -3,7 +3,8 @@ package collection // Prepend adds the given values to the beginning of the collection. // @group Transformation // @behavior mutable -// @fluent true +// @chainable true +// @terminal false // // This method mutates the collection in place and returns the same instance. // diff --git a/reduce.go b/reduce.go index 4a26185..bdfbf86 100644 --- a/reduce.go +++ b/reduce.go @@ -4,7 +4,7 @@ package collection // The accumulator has the same type T as the collection's elements. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // This is useful for computing sums, concatenations, aggregates, diff --git a/reverse.go b/reverse.go index 1003587..5007d02 100644 --- a/reverse.go +++ b/reverse.go @@ -4,7 +4,8 @@ package collection // and returns the same collection for chaining. // @group Ordering // @behavior mutable -// @fluent true +// @chainable true +// @terminal false // // This operation performs no allocations. // diff --git a/shuffle.go b/shuffle.go index e13391e..56ba341 100644 --- a/shuffle.go +++ b/shuffle.go @@ -18,7 +18,8 @@ func setShuffleRand(r *rand.Rand) { // Shuffle returns a shuffled copy of the collection. // @group Ordering // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // This operation allocates a new slice and does not mutate the receiver. // diff --git a/skip.go b/skip.go index 8802c05..15cfdd7 100644 --- a/skip.go +++ b/skip.go @@ -6,7 +6,8 @@ package collection // an empty collection. // @group Slicing // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // This operation performs no element allocations; it re-slices the // underlying slice. diff --git a/skip_last.go b/skip_last.go index 8c4cbba..cba2ca6 100644 --- a/skip_last.go +++ b/skip_last.go @@ -6,7 +6,8 @@ package collection // an empty collection. // @group Slicing // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // This operation performs no element allocations; it re-slices the // underlying slice. diff --git a/sort.go b/sort.go index 3a81cc5..2a5d83c 100644 --- a/sort.go +++ b/sort.go @@ -6,7 +6,8 @@ import "sort" // returns the same collection for chaining. // @group Ordering // @behavior mutable -// @fluent true +// @chainable true +// @terminal false // // The comparison function `less(a, b)` should return true if `a` should come // before `b` in the sorted order. diff --git a/sum.go b/sum.go index 1580281..2205f02 100644 --- a/sum.go +++ b/sum.go @@ -4,7 +4,7 @@ package collection // If the collection is empty, Sum returns the zero value of T. // @group Aggregation // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // Example: integers diff --git a/symmetric_difference.go b/symmetric_difference.go index 3151e7e..58d159a 100644 --- a/symmetric_difference.go +++ b/symmetric_difference.go @@ -5,7 +5,8 @@ package collection // its unique items, then the second for its unique items. Duplicates are removed. // @group Set Operations // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Example: integers // diff --git a/take.go b/take.go index 94159d5..0d23d4e 100644 --- a/take.go +++ b/take.go @@ -2,7 +2,8 @@ package collection // Take returns a new collection containing the first `n` items when n > 0, // or the last `|n|` items when n < 0. -// @fluent true +// @chainable true +// @terminal false // // If n exceeds the collection length, the entire collection is returned. // If n == 0, an empty collection is returned. diff --git a/take_last.go b/take_last.go index ea507ad..e24feb8 100644 --- a/take_last.go +++ b/take_last.go @@ -4,7 +4,8 @@ package collection // 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. -// @fluent true +// @chainable true +// @terminal false // // This operation performs no element allocations; it re-slices the // underlying slice. diff --git a/take_until.go b/take_until.go index 0181ddc..285e185 100644 --- a/take_until.go +++ b/take_until.go @@ -4,7 +4,8 @@ package collection // The matching item is NOT included. // @group Slicing // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // NOTE: returns a view (shares backing array). Use Clone() to detach. // Example: integers - stop when value >= 3 @@ -49,7 +50,8 @@ func (c *Collection[T]) TakeUntilFn(pred func(T) bool) *Collection[T] { // TakeUntil returns items until the first element equals `value`. // The matching item is NOT included. -// @fluent true +// @chainable true +// @terminal false // // Uses == comparison, so T must be comparable. // @group Slicing diff --git a/tap.go b/tap.go index 7800eaa..c168bad 100644 --- a/tap.go +++ b/tap.go @@ -4,7 +4,8 @@ package collection // inspection) and returns the same collection to allow chaining. // @group Transformation // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Tap does NOT modify the collection itself; it simply exposes the current state // during a fluent chain. diff --git a/terminal_allocs_test.go b/terminal_allocs_test.go new file mode 100644 index 0000000..4703c9e --- /dev/null +++ b/terminal_allocs_test.go @@ -0,0 +1,117 @@ +package collection + +import "testing" + +var ( + allocCollection *Collection[int] + allocNumeric *NumericCollection[int] + + allocBool bool + allocInt int + allocValue int +) + +func allocPred(v int) bool { + return v%2 == 0 +} + +func allocReduce(acc, v int) int { + return acc + v +} + +func allocAny() { + allocBool = allocCollection.Any(allocPred) +} + +func allocAll() { + allocBool = allocCollection.All(allocPred) +} + +func allocNone() { + allocBool = allocCollection.None(allocPred) +} + +func allocFirst() { + var ok bool + allocValue, ok = allocCollection.First() + allocBool = ok +} + +func allocLast() { + var ok bool + allocValue, ok = allocCollection.Last() + allocBool = ok +} + +func allocFirstWhere() { + var ok bool + allocValue, ok = allocCollection.FirstWhere(allocPred) + allocBool = ok +} + +func allocIndexWhere() { + var ok bool + allocInt, ok = allocCollection.IndexWhere(allocPred) + allocBool = ok +} + +func allocReduceSum() { + allocInt = allocCollection.Reduce(0, allocReduce) +} + +func allocSum() { + allocInt = allocNumeric.Sum() +} + +func allocMin() { + var ok bool + allocInt, ok = allocNumeric.Min() + allocBool = ok +} + +func allocMax() { + var ok bool + allocInt, ok = allocNumeric.Max() + allocBool = ok +} + +func allocContains() { + allocBool = Contains(allocCollection, 4) +} + +func allocIsEmpty() { + allocBool = allocCollection.IsEmpty() +} + +func TestTerminalOps_ZeroAlloc(t *testing.T) { + allocCollection = New([]int{1, 2, 3, 4, 5, 6}) + allocNumeric = NewNumeric([]int{1, 2, 3, 4, 5, 6}) + + cases := []struct { + name string + fn func() + }{ + {name: "Any", fn: allocAny}, + {name: "All", fn: allocAll}, + {name: "None", fn: allocNone}, + {name: "First", fn: allocFirst}, + {name: "Last", fn: allocLast}, + {name: "FirstWhere", fn: allocFirstWhere}, + {name: "IndexWhere", fn: allocIndexWhere}, + {name: "Reduce", fn: allocReduceSum}, + {name: "Sum", fn: allocSum}, + {name: "Min", fn: allocMin}, + {name: "Max", fn: allocMax}, + {name: "Contains", fn: allocContains}, + {name: "IsEmpty", fn: allocIsEmpty}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + allocs := testing.AllocsPerRun(1000, tc.fn) + if allocs != 0 { + t.Fatalf("%s should allocate 0 times; got %v", tc.name, allocs) + } + }) + } +} diff --git a/times.go b/times.go index e1f958b..253791e 100644 --- a/times.go +++ b/times.go @@ -4,7 +4,8 @@ package collection // This mirrors Laravel's Collection::times(), which is 1-indexed. // @group Transformation // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // If count <= 0, an empty collection is returned. // diff --git a/to_json.go b/to_json.go index 6ae5c7d..c51650c 100644 --- a/to_json.go +++ b/to_json.go @@ -8,7 +8,7 @@ import ( // ToJSON converts the collection's items into a compact JSON string. // @group Serialization // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // If marshalling succeeds, a JSON-encoded string and a nil error are returned. @@ -37,7 +37,7 @@ func (c *Collection[T]) ToJSON() (string, error) { // indented JSON string. // @group Serialization // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // If marshalling succeeds, a formatted JSON string and nil error are returned. diff --git a/to_map.go b/to_map.go index 947612d..f197ab9 100644 --- a/to_map.go +++ b/to_map.go @@ -4,7 +4,7 @@ package collection // selector functions. // @group Maps // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // If multiple items produce the same key, the last value wins. diff --git a/to_map_kv.go b/to_map_kv.go index 772f4d7..0925a85 100644 --- a/to_map_kv.go +++ b/to_map_kv.go @@ -3,7 +3,7 @@ package collection // ToMapKV converts a collection of key/value pairs into a map. // @group Maps // @behavior readonly -// @fluent false +// @chainable false // @terminal true // // If multiple pairs contain the same key, the last value wins. diff --git a/transform.go b/transform.go index a5a937e..d35cd50 100644 --- a/transform.go +++ b/transform.go @@ -3,7 +3,7 @@ package collection // Transform applies fn to every item *in place*, mutating the collection. // @group Transformation // @behavior mutable -// @fluent false +// @chainable false // @terminal true // // This mirrors Laravel's transform(), which modifies the underlying values diff --git a/union.go b/union.go index 1877777..4e6a482 100644 --- a/union.go +++ b/union.go @@ -5,7 +5,8 @@ package collection // that were not already present. // @group Set Operations // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Example: integers // diff --git a/unique.go b/unique.go index ae535d7..45587a0 100644 --- a/unique.go +++ b/unique.go @@ -5,7 +5,8 @@ package collection // and order is preserved. // @group Set Operations // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // The `eq` function should return true when two values are considered equal. // diff --git a/unique_by.go b/unique_by.go index 134d3fc..8310b56 100644 --- a/unique_by.go +++ b/unique_by.go @@ -4,7 +4,8 @@ package collection // of each element as determined by keyFn. // @group Set Operations // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // The key returned by keyFn must be comparable. // Order is preserved. diff --git a/unique_comparable.go b/unique_comparable.go index e7817a9..00b4ee5 100644 --- a/unique_comparable.go +++ b/unique_comparable.go @@ -5,7 +5,8 @@ package collection // This is a faster, allocation-friendly path for comparable types. // @group Set Operations // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Example: integers // diff --git a/window.go b/window.go index daa643a..aaf8c07 100644 --- a/window.go +++ b/window.go @@ -5,7 +5,8 @@ package collection // Windows that are shorter than size are omitted. // @group Slicing // @behavior allocates -// @fluent true +// @chainable true +// @terminal false // // NOTE: windows share the backing array with the source collection. // diff --git a/zip.go b/zip.go index d81253d..d97a94a 100644 --- a/zip.go +++ b/zip.go @@ -4,7 +4,8 @@ package collection // The resulting length is the smaller of the two inputs. // @group Transformation // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Example: integers and strings // @@ -74,7 +75,8 @@ func Zip[A any, B any](a *Collection[A], b *Collection[B]) *Collection[Tuple[A, // The resulting length is the smaller of the two inputs. // @group Transformation // @behavior immutable -// @fluent true +// @chainable true +// @terminal false // // Example: sum ints // From c42dbc79fe9f8f87a6f7adf325f9c3f397b7a3b5 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 01:27:59 -0600 Subject: [PATCH 16/30] docs: benchmarks --- BENCHMARKS.md | 118 ++++++++++++++++++++++----------------------- README.md | 78 ++++-------------------------- docs/bench/main.go | 17 ++----- partition.go | 4 +- 4 files changed, 76 insertions(+), 141 deletions(-) diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 0526fa2..df26246 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -4,74 +4,74 @@ Raw results for `collection.New` (borrowed) vs `lo`. | Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 255ns / 238ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 254ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 141ns / 1.1µs | **7.84x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 253ns / 239ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.3µs / 8.6µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.3µs / 8.3µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 19.8µs / 44.9µs | **2.27x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **All** | 262ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 256ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 140ns / 1.1µs | **7.57x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 257ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.3µs / 8.5µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.6µs / 8.6µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 19.9µs / 45.1µs | **2.27x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | | **Each** | 258ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 663ns / 1.0µs | **1.55x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **First** | 12ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 257ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 8.2µs / 9.0µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 254ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 11.1µs / 10.8µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **Filter** | 658ns / 1.0µs | **1.57x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **First** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **FirstWhere** | 254ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 8.2µs / 8.7µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 278ns / 254ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.6µs / 10.9µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | | **Last** | 12ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 366ns / 800ns | **2.18x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Max** | 258ns / 235ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 256ns / 233ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 256ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 724ns / 1.3µs | **1.74x** | 48B / 12.3KB | **256.00x less** | 2 / 2 | -| **Reduce (sum)** | 259ns / 238ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 232ns / 239ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 4.0µs / 5.7µs | **1.43x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 11ns / 716ns | **62.99x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 12ns / 716ns | **62.29x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 258ns / 233ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Map** | 366ns / 796ns | **2.18x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Max** | 260ns / 244ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 266ns / 247ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 252ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 502ns / 1.4µs | **2.70x** | 48B / 12.3KB | **256.00x less** | 2 / 2 | +| **Reduce (sum)** | 256ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 236ns / 239ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 4.1µs / 5.7µs | **1.41x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 12ns / 727ns | **59.96x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 12ns / 717ns | **61.68x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 262ns / 241ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | | **Take** | 23ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.7µs / 7.9µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 17.5µs / 18.0µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | -| **Unique** | 6.5µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.8µs / 6.5µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.4µs / 3.3µs | **2.27x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.0µs / 3.3µs | **3.22x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **ToMap** | 7.9µs / 8.0µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 18.0µs / 18.2µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Unique** | 6.7µs / 6.7µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **UniqueBy** | 6.9µs / 6.7µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.5µs / 3.6µs | **2.44x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.1µs / 3.3µs | **3.09x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | Raw results for `collection.New().Clone()` (explicit copy) vs `lo`. | Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 916ns / 239ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Any** | 906ns / 240ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Chunk** | 846ns / 1.1µs | **1.27x** | 9.5KB / 9.3KB | ≈ | 4 / 51 | -| **Contains** | 905ns / 241ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **All** | 940ns / 242ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Any** | 906ns / 241ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Chunk** | 856ns / 1.1µs | **1.25x** | 9.5KB / 9.3KB | ≈ | 4 / 51 | +| **Contains** | 904ns / 242ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | | **CountBy** | 9.1µs / 8.5µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | -| **CountByValue** | 9.1µs / 8.5µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | -| **Difference** | 20.8µs / 44.9µs | **2.16x** | 98.6KB / 108.8KB | **1.10x less** | 18 / 43 | -| **Each** | 900ns / 233ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **CountByValue** | 9.1µs / 8.4µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | +| **Difference** | 21.1µs / 45.1µs | **2.14x** | 98.6KB / 108.8KB | **1.10x less** | 18 / 43 | +| **Each** | 911ns / 234ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | | **Filter** | 1.2µs / 1.0µs | 0.90x | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **First** | 738ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **First** | 739ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | | **FirstWhere** | 913ns / 241ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **GroupBySlice** | 8.9µs / 9.2µs | ≈ | 29.3KB / 21.0KB | 0.72x more | 86 / 83 | -| **IndexWhere** | 898ns / 240ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Intersect** | 12.3µs / 11.1µs | ≈ | 27.9KB / 11.4KB | 0.41x more | 26 / 19 | -| **Last** | 726ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Map** | 920ns / 803ns | 0.87x | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **Max** | 924ns / 233ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | -| **Min** | 918ns / 232ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | -| **None** | 901ns / 239ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Pipeline F→M→T→R** | 1.2µs / 1.3µs | ≈ | 8.3KB / 12.3KB | **1.49x less** | 4 / 2 | -| **Reduce (sum)** | 899ns / 238ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Reverse** | 840ns / 239ns | 0.28x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **GroupBySlice** | 8.9µs / 8.9µs | ≈ | 29.3KB / 21.0KB | 0.72x more | 86 / 83 | +| **IndexWhere** | 916ns / 244ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Intersect** | 12.3µs / 10.9µs | 0.89x | 27.9KB / 11.4KB | 0.41x more | 26 / 19 | +| **Last** | 729ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Map** | 924ns / 803ns | 0.87x | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **Max** | 923ns / 236ns | 0.26x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **Min** | 930ns / 241ns | 0.26x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **None** | 946ns / 245ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Pipeline F→M→T→R** | 1.1µs / 1.3µs | **1.15x** | 8.3KB / 12.3KB | **1.49x less** | 4 / 2 | +| **Reduce (sum)** | 908ns / 239ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Reverse** | 825ns / 238ns | 0.29x | 8.2KB / 0B | ∞x more | 3 / 0 | | **Shuffle** | 4.5µs / 5.7µs | **1.26x** | 16.5KB / 0B | ∞x more | 5 / 0 | -| **Skip** | 720ns / 725ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **SkipLast** | 742ns / 729ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **Sum** | 917ns / 234ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | -| **Take** | 738ns / <1ns | 0.00x | 8.3KB / 0B | ∞x more | 4 / 0 | -| **ToMap** | 8.2µs / 8.0µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 8 / 6 | -| **Union** | 18.7µs / 18.1µs | ≈ | 106.8KB / 90.3KB | 0.85x more | 17 / 10 | -| **Unique** | 7.1µs / 6.4µs | 0.89x | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | -| **UniqueBy** | 7.4µs / 6.5µs | 0.87x | 53.4KB / 45.1KB | 0.85x more | 10 / 6 | -| **Zip** | 2.8µs / 3.3µs | **1.19x** | 32.9KB / 16.4KB | 0.50x more | 7 / 1 | -| **ZipWith** | 2.4µs / 3.3µs | **1.37x** | 24.7KB / 8.2KB | 0.33x more | 7 / 1 | +| **Skip** | 722ns / 723ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **SkipLast** | 728ns / 723ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **Sum** | 927ns / 241ns | 0.26x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **Take** | 734ns / <1ns | 0.00x | 8.3KB / 0B | ∞x more | 4 / 0 | +| **ToMap** | 8.5µs / 8.2µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 8 / 6 | +| **Union** | 18.9µs / 18.2µs | ≈ | 106.8KB / 90.3KB | 0.85x more | 17 / 10 | +| **Unique** | 7.2µs / 6.4µs | 0.89x | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | +| **UniqueBy** | 7.5µs / 6.5µs | 0.87x | 53.4KB / 45.1KB | 0.85x more | 10 / 6 | +| **Zip** | 2.8µs / 3.3µs | **1.18x** | 32.9KB / 16.4KB | 0.50x more | 7 / 1 | +| **ZipWith** | 2.4µs / 3.2µs | **1.33x** | 24.7KB / 8.2KB | 0.33x more | 7 / 1 | diff --git a/README.md b/README.md index 0908eb3..cb96ca0 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,6 @@ That design choice doesn't matter much for some single operations. It matters a Full raw tables: see `BENCHMARKS.md`. -### Borrow-by-default (New) - #### Read-only scalar ops (wrapper overhead only) | Op | Speed vs lo | Memory | Allocs | @@ -117,12 +115,12 @@ Full raw tables: see `BENCHMARKS.md`. | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Chunk** | **7.84x** | -8.0KB | -49 | +| **Chunk** | **7.57x** | -8.0KB | -49 | | **Take** | ≈ | +48B | +2 | -| **Skip** | **62.99x** | -8.2KB | ≈ | -| **SkipLast** | **62.29x** | -8.2KB | ≈ | -| **Zip** | **2.27x** | +48B | +2 | -| **ZipWith** | **3.22x** | +48B | +2 | +| **Skip** | **59.96x** | -8.2KB | ≈ | +| **SkipLast** | **61.68x** | -8.2KB | ≈ | +| **Zip** | **2.44x** | +48B | +2 | +| **ZipWith** | **3.09x** | +48B | +2 | | **Unique** | ≈ | +24B | +1 | | **UniqueBy** | ≈ | +48B | +2 | | **Union** | ≈ | +72B | +3 | @@ -137,72 +135,16 @@ Full raw tables: see `BENCHMARKS.md`. | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Pipeline F→M→T→R** | **1.74x** | -12.2KB | ≈ | +| **Pipeline F→M→T→R** | **2.70x** | -12.2KB | ≈ | #### Mutating ops | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| | **Map** | **2.18x** | -8.2KB | ≈ | -| **Filter** | **1.55x** | -8.2KB | ≈ | +| **Filter** | **1.57x** | -8.2KB | ≈ | | **Reverse** | ≈ | +24B | +1 | -| **Shuffle** | **1.43x** | +8.2KB | +3 | - - -### Explicit copy cost (Clone) - -#### Read-only scalar ops (wrapper overhead only) - -| Op | Speed vs lo | Memory | Allocs | -|---:|:-----------:|:------:|:------:| -| **All** | 0.26x | +8.2KB | +3 | -| **Any** | 0.27x | +8.2KB | +3 | -| **None** | 0.27x | +8.2KB | +3 | -| **First** | 0.00x | +8.2KB | +3 | -| **Last** | 0.00x | +8.2KB | +3 | -| **FirstWhere** | 0.26x | +8.2KB | +3 | -| **IndexWhere** | 0.27x | +8.2KB | +3 | -| **Contains** | 0.27x | +8.2KB | +3 | -| **Reduce (sum)** | 0.26x | +8.2KB | +3 | -| **Sum** | 0.25x | +8.3KB | +5 | -| **Min** | 0.25x | +8.3KB | +5 | -| **Max** | 0.25x | +8.3KB | +5 | -| **Each** | 0.26x | +8.2KB | +3 | - -#### Transforming ops - -| Op | Speed vs lo | Memory | Allocs | -|---:|:-----------:|:------:|:------:| -| **Chunk** | **1.27x** | +240B | -47 | -| **Take** | 0.00x | +8.3KB | +4 | -| **Skip** | ≈ | +48B | +2 | -| **SkipLast** | ≈ | +48B | +2 | -| **Zip** | **1.19x** | +16.5KB | +6 | -| **ZipWith** | **1.37x** | +16.5KB | +6 | -| **Unique** | 0.89x | +8.2KB | +3 | -| **UniqueBy** | 0.87x | +8.3KB | +4 | -| **Union** | ≈ | +16.5KB | +7 | -| **Intersect** | ≈ | +16.5KB | +7 | -| **Difference** | **2.16x** | -10.2KB | -25 | -| **GroupBySlice** | ≈ | +8.2KB | +3 | -| **CountBy** | ≈ | +8.2KB | +3 | -| **CountByValue** | ≈ | +8.2KB | +3 | -| **ToMap** | ≈ | +8.2KB | +2 | - -#### Pipelines - -| Op | Speed vs lo | Memory | Allocs | -|---:|:-----------:|:------:|:------:| -| **Pipeline F→M→T→R** | ≈ | -4.0KB | +2 | - -#### Mutating ops - -| Op | Speed vs lo | Memory | Allocs | -|---:|:-----------:|:------:|:------:| -| **Map** | 0.87x | +48B | +2 | -| **Filter** | 0.90x | +48B | +2 | -| **Reverse** | 0.28x | +8.2KB | +3 | -| **Shuffle** | **1.26x** | +16.5KB | +5 | +| **Shuffle** | **1.41x** | +8.2KB | +3 | ## How to read the benchmarks @@ -229,7 +171,7 @@ That model is simple and safe - but each step typically allocates. `collection` pipelines are designed to look more like this: ``` -input → Filter (in place) → Sort (in place) → Take (slice view) +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: @@ -2710,7 +2652,7 @@ collection.Dump(users.Items()) // ] ``` -### Partition · immutable · chainable +### 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 diff --git a/docs/bench/main.go b/docs/bench/main.go index ba649d3..3297c45 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -40,15 +40,14 @@ func main() { only := parseOnly(*onlyFlag) borrowResults := runBenches(only, benchBorrow) copyResults := runBenches(only, benchCopy) - condensed := renderCondensedTables(borrowResults, copyResults) + condensed := renderCondensedTables(borrowResults) rawBorrow := renderTable(borrowResults) - rawCopy := renderTable(copyResults) if err := updateReadme(condensed); err != nil { fmt.Println("Error:", err) os.Exit(1) } - if err := updateBenchmarksFile(rawBorrow, rawCopy); err != nil { + if err := updateBenchmarksFile(rawBorrow, renderTable(copyResults)); err != nil { fmt.Println("Error:", err) os.Exit(1) } @@ -983,7 +982,7 @@ type benchGroup struct { ops []string } -func renderCondensedTables(borrowResults, copyResults []benchResult) string { +func renderCondensedTables(results []benchResult) string { groups := []benchGroup{ { name: "Read-only scalar ops (wrapper overhead only)", @@ -1042,13 +1041,6 @@ func renderCondensedTables(borrowResults, copyResults []benchResult) string { var buf bytes.Buffer buf.WriteString("Full raw tables: see `BENCHMARKS.md`.\n\n") - renderCondensedTable(&buf, "Borrow-by-default (New)", borrowResults, groups) - buf.WriteString("\n") - renderCondensedTable(&buf, "Explicit copy cost (Clone)", copyResults, groups) - return strings.TrimSpace(buf.String()) -} - -func renderCondensedTable(buf *bytes.Buffer, title string, results []benchResult, groups []benchGroup) { byName := map[string]map[string]benchResult{} for _, r := range results { if _, ok := byName[r.name]; !ok { @@ -1057,7 +1049,6 @@ func renderCondensedTable(buf *bytes.Buffer, title string, results []benchResult byName[r.name][r.impl] = r } - buf.WriteString(fmt.Sprintf("### %s\n\n", title)) for _, group := range groups { rows := make([]string, 0, len(group.ops)) for _, name := range group.ops { @@ -1087,6 +1078,8 @@ func renderCondensedTable(buf *bytes.Buffer, title string, results []benchResult buf.WriteString(strings.Join(rows, "\n")) buf.WriteString("\n\n") } + + return strings.TrimSpace(buf.String()) } func formatNs(ns float64) string { diff --git a/partition.go b/partition.go index 2c92143..c600049 100644 --- a/partition.go +++ b/partition.go @@ -5,8 +5,8 @@ package collection // items where fn returns false. Order is preserved within each partition. // @group Slicing // @behavior immutable -// @chainable true -// @terminal false +// @chainable false +// @terminal true // // Example: integers - even/odd // From 9d46fbf49ab13003fcfa182c6f6c3eac9ea805db Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 01:43:58 -0600 Subject: [PATCH 17/30] docs: readme --- README.md | 2 +- prepend.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cb96ca0..5ac7b9a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- goforj/collection logo + goforj/collection logo

diff --git a/prepend.go b/prepend.go index 408d151..f4dd7e3 100644 --- a/prepend.go +++ b/prepend.go @@ -7,6 +7,7 @@ package collection // @terminal false // // This method mutates the collection in place and returns the same instance. +// It allocates a new backing slice to insert the values at the front. // // Example: integers // From 1c3d41d999e8d479746ea4fe95d1fdb80c1e04a7 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 01:44:17 -0600 Subject: [PATCH 18/30] docs: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ac7b9a..b9e2c84 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- goforj/collection logo + goforj/collection logo

From 7840f4d4ef7ee359627656c4d99c966b2e014afb Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 01:58:19 -0600 Subject: [PATCH 19/30] docs: benchmarks --- BENCHMARKS.md | 130 +++++++++++++++++++-------------------- README.md | 32 +++++----- examples/shuffle/main.go | 10 +-- shuffle.go | 25 ++++---- shuffle_test.go | 10 +-- 5 files changed, 102 insertions(+), 105 deletions(-) diff --git a/BENCHMARKS.md b/BENCHMARKS.md index df26246..ffc2965 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -4,74 +4,74 @@ Raw results for `collection.New` (borrowed) vs `lo`. | Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 262ns / 240ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 256ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 140ns / 1.1µs | **7.57x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 257ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.3µs / 8.5µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.6µs / 8.6µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 19.9µs / 45.1µs | **2.27x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | -| **Each** | 258ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 658ns / 1.0µs | **1.57x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **All** | 251ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 141ns / 1.1µs | **7.49x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.2µs / 8.0µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.2µs / 8.1µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 19.1µs / 44.2µs | **2.31x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 253ns / 231ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 644ns / 1.1µs | **1.64x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | | **First** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 254ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 8.2µs / 8.7µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 278ns / 254ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 11.6µs / 10.9µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | -| **Last** | 12ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 366ns / 796ns | **2.18x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Max** | 260ns / 244ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 266ns / 247ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 252ns / 237ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 502ns / 1.4µs | **2.70x** | 48B / 12.3KB | **256.00x less** | 2 / 2 | -| **Reduce (sum)** | 256ns / 241ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 236ns / 239ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 4.1µs / 5.7µs | **1.41x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 12ns / 727ns | **59.96x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 12ns / 717ns | **61.68x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 262ns / 241ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 23ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.9µs / 8.0µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 18.0µs / 18.2µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | -| **Unique** | 6.7µs / 6.7µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.9µs / 6.7µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.5µs / 3.6µs | **2.44x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.1µs / 3.3µs | **3.09x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **FirstWhere** | 249ns / 234ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 8.0µs / 8.4µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 250ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.0µs / 10.7µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **Last** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Map** | 358ns / 819ns | **2.29x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Max** | 254ns / 231ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 255ns / 236ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 248ns / 234ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 504ns / 1.3µs | **2.53x** | 48B / 12.3KB | **256.00x less** | 2 / 2 | +| **Reduce (sum)** | 253ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 227ns / 235ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 3.5µs / 5.5µs | **1.56x** | 24B / 0B | ∞x more | 1 / 0 | +| **Skip** | 11ns / 726ns | **65.27x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 11ns / 730ns | **64.89x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 255ns / 236ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 22ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.6µs / 8.0µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 17.2µs / 17.9µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Unique** | 6.4µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **UniqueBy** | 6.8µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.5µs / 3.2µs | **2.22x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.0µs / 3.0µs | **2.87x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | Raw results for `collection.New().Clone()` (explicit copy) vs `lo`. | Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 940ns / 242ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Any** | 906ns / 241ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Chunk** | 856ns / 1.1µs | **1.25x** | 9.5KB / 9.3KB | ≈ | 4 / 51 | -| **Contains** | 904ns / 242ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **CountBy** | 9.1µs / 8.5µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | -| **CountByValue** | 9.1µs / 8.4µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | -| **Difference** | 21.1µs / 45.1µs | **2.14x** | 98.6KB / 108.8KB | **1.10x less** | 18 / 43 | -| **Each** | 911ns / 234ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Filter** | 1.2µs / 1.0µs | 0.90x | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **First** | 739ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **FirstWhere** | 913ns / 241ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **GroupBySlice** | 8.9µs / 8.9µs | ≈ | 29.3KB / 21.0KB | 0.72x more | 86 / 83 | -| **IndexWhere** | 916ns / 244ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Intersect** | 12.3µs / 10.9µs | 0.89x | 27.9KB / 11.4KB | 0.41x more | 26 / 19 | -| **Last** | 729ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Map** | 924ns / 803ns | 0.87x | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **Max** | 923ns / 236ns | 0.26x | 8.3KB / 0B | ∞x more | 5 / 0 | -| **Min** | 930ns / 241ns | 0.26x | 8.3KB / 0B | ∞x more | 5 / 0 | -| **None** | 946ns / 245ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Pipeline F→M→T→R** | 1.1µs / 1.3µs | **1.15x** | 8.3KB / 12.3KB | **1.49x less** | 4 / 2 | -| **Reduce (sum)** | 908ns / 239ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Reverse** | 825ns / 238ns | 0.29x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Shuffle** | 4.5µs / 5.7µs | **1.26x** | 16.5KB / 0B | ∞x more | 5 / 0 | -| **Skip** | 722ns / 723ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **SkipLast** | 728ns / 723ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **Sum** | 927ns / 241ns | 0.26x | 8.3KB / 0B | ∞x more | 5 / 0 | -| **Take** | 734ns / <1ns | 0.00x | 8.3KB / 0B | ∞x more | 4 / 0 | -| **ToMap** | 8.5µs / 8.2µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 8 / 6 | -| **Union** | 18.9µs / 18.2µs | ≈ | 106.8KB / 90.3KB | 0.85x more | 17 / 10 | -| **Unique** | 7.2µs / 6.4µs | 0.89x | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | -| **UniqueBy** | 7.5µs / 6.5µs | 0.87x | 53.4KB / 45.1KB | 0.85x more | 10 / 6 | -| **Zip** | 2.8µs / 3.3µs | **1.18x** | 32.9KB / 16.4KB | 0.50x more | 7 / 1 | -| **ZipWith** | 2.4µs / 3.2µs | **1.33x** | 24.7KB / 8.2KB | 0.33x more | 7 / 1 | +| **All** | 916ns / 234ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Any** | 920ns / 235ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Chunk** | 862ns / 1.1µs | **1.26x** | 9.5KB / 9.3KB | ≈ | 4 / 51 | +| **Contains** | 924ns / 234ns | 0.25x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **CountBy** | 8.9µs / 8.6µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | +| **CountByValue** | 9.0µs / 15.2µs | **1.68x** | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | +| **Difference** | 20.6µs / 44.8µs | **2.17x** | 98.6KB / 108.8KB | **1.10x less** | 18 / 43 | +| **Each** | 913ns / 232ns | 0.25x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Filter** | 1.2µs / 1.0µs | 0.89x | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **First** | 737ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **FirstWhere** | 934ns / 236ns | 0.25x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **GroupBySlice** | 8.7µs / 8.7µs | ≈ | 29.3KB / 21.0KB | 0.72x more | 86 / 83 | +| **IndexWhere** | 925ns / 234ns | 0.25x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Intersect** | 12.2µs / 10.9µs | 0.90x | 27.9KB / 11.4KB | 0.41x more | 26 / 19 | +| **Last** | 740ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Map** | 958ns / 816ns | 0.85x | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **Max** | 930ns / 231ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **Min** | 925ns / 236ns | 0.26x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **None** | 916ns / 236ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Pipeline F→M→T→R** | 1.1µs / 1.3µs | **1.21x** | 8.3KB / 12.3KB | **1.49x less** | 4 / 2 | +| **Reduce (sum)** | 911ns / 236ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Reverse** | 841ns / 240ns | 0.29x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Shuffle** | 4.2µs / 5.7µs | **1.34x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 771ns / 982ns | **1.27x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **SkipLast** | 2.2µs / 736ns | 0.33x | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **Sum** | 918ns / 236ns | 0.26x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **Take** | 746ns / <1ns | 0.00x | 8.3KB / 0B | ∞x more | 4 / 0 | +| **ToMap** | 8.3µs / 8.0µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 8 / 6 | +| **Union** | 18.6µs / 18.1µs | ≈ | 106.8KB / 90.3KB | 0.85x more | 17 / 10 | +| **Unique** | 7.1µs / 6.4µs | ≈ | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | +| **UniqueBy** | 7.5µs / 6.5µs | 0.86x | 53.4KB / 45.1KB | 0.85x more | 10 / 6 | +| **Zip** | 2.8µs / 3.3µs | **1.17x** | 32.9KB / 16.4KB | 0.50x more | 7 / 1 | +| **ZipWith** | 2.4µs / 3.0µs | **1.26x** | 24.7KB / 8.2KB | 0.33x more | 7 / 1 | diff --git a/README.md b/README.md index b9e2c84..b662d66 100644 --- a/README.md +++ b/README.md @@ -115,17 +115,17 @@ Full raw tables: see `BENCHMARKS.md`. | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Chunk** | **7.57x** | -8.0KB | -49 | +| **Chunk** | **7.49x** | -8.0KB | -49 | | **Take** | ≈ | +48B | +2 | -| **Skip** | **59.96x** | -8.2KB | ≈ | -| **SkipLast** | **61.68x** | -8.2KB | ≈ | -| **Zip** | **2.44x** | +48B | +2 | -| **ZipWith** | **3.09x** | +48B | +2 | +| **Skip** | **65.27x** | -8.2KB | ≈ | +| **SkipLast** | **64.89x** | -8.2KB | ≈ | +| **Zip** | **2.22x** | +48B | +2 | +| **ZipWith** | **2.87x** | +48B | +2 | | **Unique** | ≈ | +24B | +1 | | **UniqueBy** | ≈ | +48B | +2 | | **Union** | ≈ | +72B | +3 | | **Intersect** | ≈ | +72B | +3 | -| **Difference** | **2.27x** | -26.7KB | -29 | +| **Difference** | **2.31x** | -26.7KB | -29 | | **GroupBySlice** | ≈ | +24B | +1 | | **CountBy** | ≈ | +24B | +1 | | **CountByValue** | ≈ | +24B | +1 | @@ -135,16 +135,16 @@ Full raw tables: see `BENCHMARKS.md`. | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Pipeline F→M→T→R** | **2.70x** | -12.2KB | ≈ | +| **Pipeline F→M→T→R** | **2.53x** | -12.2KB | ≈ | #### Mutating ops | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Map** | **2.18x** | -8.2KB | ≈ | -| **Filter** | **1.57x** | -8.2KB | ≈ | +| **Map** | **2.29x** | -8.2KB | ≈ | +| **Filter** | **1.64x** | -8.2KB | ≈ | | **Reverse** | ≈ | +24B | +1 | -| **Shuffle** | **1.41x** | +8.2KB | +3 | +| **Shuffle** | **1.56x** | +24B | +1 | ## How to read the benchmarks @@ -1470,16 +1470,16 @@ collection.Dump(users.Items()) // ] ``` -### Shuffle · immutable · chainable +### Shuffle · mutable · chainable -Shuffle returns a shuffled copy of the collection. +Shuffle shuffles the collection in place and returns the same collection. _Example: integers_ ```go c := collection.New([]int{1, 2, 3, 4, 5}) -out1 := c.Shuffle() -collection.Dump(out1.Items()) +c.Shuffle() +collection.Dump(c.Items()) ``` _Example: strings – chaining_ @@ -1507,8 +1507,8 @@ users := collection.New([]User{ {ID: 4}, }) -out3 := users.Shuffle() -collection.Dump(out3.Items()) +users.Shuffle() +collection.Dump(users.Items()) ``` ### Sort · mutable · chainable diff --git a/examples/shuffle/main.go b/examples/shuffle/main.go index 90f4da6..7735c4a 100644 --- a/examples/shuffle/main.go +++ b/examples/shuffle/main.go @@ -6,12 +6,12 @@ package main import "github.com/goforj/collection" func main() { - // Shuffle returns a shuffled copy of the collection. + // Shuffle shuffles the collection in place and returns the same collection. // Example: integers c := collection.New([]int{1, 2, 3, 4, 5}) - out1 := c.Shuffle() - collection.Dump(out1.Items()) + c.Shuffle() + collection.Dump(c.Items()) // Example: strings – chaining out2 := collection.New([]string{"a", "b", "c"}). @@ -33,6 +33,6 @@ func main() { {ID: 4}, }) - out3 := users.Shuffle() - collection.Dump(out3.Items()) + users.Shuffle() + collection.Dump(users.Items()) } diff --git a/shuffle.go b/shuffle.go index 56ba341..3c021c7 100644 --- a/shuffle.go +++ b/shuffle.go @@ -15,13 +15,13 @@ func setShuffleRand(r *rand.Rand) { shuffleRand = r } -// Shuffle returns a shuffled copy of the collection. +// Shuffle shuffles the collection in place and returns the same collection. // @group Ordering -// @behavior immutable +// @behavior mutable // @chainable true // @terminal false // -// This operation allocates a new slice and does not mutate the receiver. +// This operation mutates the receiver's backing slice. // // The shuffle uses an internal random source. Tests may override // this source to achieve deterministic behavior. @@ -29,8 +29,8 @@ func setShuffleRand(r *rand.Rand) { // Example: integers // // c := collection.New([]int{1, 2, 3, 4, 5}) -// out1 := c.Shuffle() -// collection.Dump(out1.Items()) +// c.Shuffle() +// collection.Dump(c.Items()) // // Example: strings – chaining // @@ -54,19 +54,16 @@ func setShuffleRand(r *rand.Rand) { // {ID: 4}, // }) // -// out3 := users.Shuffle() -// collection.Dump(out3.Items()) +// users.Shuffle() +// collection.Dump(users.Items()) func (c *Collection[T]) Shuffle() *Collection[T] { - items := c.items - out := make([]T, len(items)) - copy(out, items) - n := len(out) + n := len(c.items) - // Fisher–Yates shuffle (in place on the copy) + // Fisher–Yates shuffle (in place) for i := n - 1; i > 0; i-- { j := shuffleRand.Intn(i + 1) - out[i], out[j] = out[j], out[i] + c.items[i], c.items[j] = c.items[j], c.items[i] } - return New(out) + return c } diff --git a/shuffle_test.go b/shuffle_test.go index 6f85aef..aa989fe 100644 --- a/shuffle_test.go +++ b/shuffle_test.go @@ -103,19 +103,19 @@ func TestShuffle_Structs(t *testing.T) { }) } -func TestShuffle_DoesNotMutateOriginal(t *testing.T) { +func TestShuffle_MutatesInPlace(t *testing.T) { withDeterministicShuffle(t, 99, func() { items := []int{1, 2, 3, 4} c := New(items) out := c.Shuffle() - if !reflect.DeepEqual(items, []int{1, 2, 3, 4}) { - t.Fatalf("expected source slice to remain unchanged") + if out != c { + t.Fatalf("Shuffle should return the same collection") } - if reflect.DeepEqual(out.Items(), []int{1, 2, 3, 4}) { - t.Fatalf("expected shuffled output to differ from original order") + if reflect.DeepEqual(items, []int{1, 2, 3, 4}) { + t.Fatalf("expected source slice to be shuffled in place") } }) } From a10a107048e0cd2eff45a903fe6345f53151e844 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 02:00:55 -0600 Subject: [PATCH 20/30] docs: readme --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b662d66..0399cd5 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,11 @@ # Features +This library is ideal when: +- you write multi-step slice pipelines +- allocation behavior matters +- you want fluent APIs without hidden costs + - **Fluent chaining** - pipeline your operations like Laravel Collections - **Fully generic** (`Collection[T]`) - no reflection, no `interface{}` - **Minimal dependencies** - small footprint (godump for debugging) @@ -43,7 +48,7 @@ Many methods return the collection itself, allowing for fluent method chaining. -Some methods maybe limited to due to go's generic constraints. +Some methods may be limited due to Go's generic constraints. > **Fluent example:** > [`examples/chaining/main.go`](./examples/chaining/main.go) @@ -150,7 +155,7 @@ Full raw tables: see `BENCHMARKS.md`. ## How to read the benchmarks * **≈** means the two libraries are effectively equivalent -* **∞x less** means one side allocates while the other allocates nothing +* 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 @@ -947,7 +952,7 @@ collection.Dump(odds.Items()) ### New · immutable · chainable -New creates a new Collection from the provided slice and borrows it. +New creates a new Collection that borrows the provided slice without copying. ### NewNumeric · immutable · chainable From 2ee93a91fd742e124e3865b200953d06d2032b98 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 02:11:55 -0600 Subject: [PATCH 21/30] docs: readme fix --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 0399cd5..680f5e3 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,6 @@ # Features -This library is ideal when: -- you write multi-step slice pipelines -- allocation behavior matters -- you want fluent APIs without hidden costs - - **Fluent chaining** - pipeline your operations like Laravel Collections - **Fully generic** (`Collection[T]`) - no reflection, no `interface{}` - **Minimal dependencies** - small footprint (godump for debugging) From 67c915ec650a955e7d18ae912e36974320a614ff Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 02:12:22 -0600 Subject: [PATCH 22/30] docs: readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 680f5e3..de38488 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,13 @@ - **Developer-friendly debug helpers** (`Dump()`, `Dd()`, `DumpStr()`) - **Works with any Go type**, including structs, pointers, and deeply nested composites +# Why Collection? + +This library is ideal when: +- you write multi-step slice pipelines +- allocation behavior matters +- you want fluent APIs without hidden costs + ## Fluent Chaining Many methods return the collection itself, allowing for fluent method chaining. From 0d19434126029dc1d73e25fa2dc6e0eef02190af Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 02:12:59 -0600 Subject: [PATCH 23/30] docs: readme tweak --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index de38488..436171e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Designed to feel natural to Go developers - and luxurious to everyone else.

-# Features +## Features - **Fluent chaining** - pipeline your operations like Laravel Collections - **Fully generic** (`Collection[T]`) - no reflection, no `interface{}` @@ -39,7 +39,7 @@ - **Developer-friendly debug helpers** (`Dump()`, `Dd()`, `DumpStr()`) - **Works with any Go type**, including structs, pointers, and deeply nested composites -# Why Collection? +## Why Collection? This library is ideal when: - you write multi-step slice pipelines From 2b4b5d20b9280ceeed965337015960bc0b5d5a65 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 02:13:57 -0600 Subject: [PATCH 24/30] docs: readme --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 436171e..da6ccbd 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,8 @@

- collection brings an expressive, fluent API to Go. - Iterate, filter, transform, sort, reduce, group, and debug your data with zero dependencies. - Designed to feel natural to Go developers - and luxurious to everyone else. + 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 From 5543405e9eba0d04ca7489c7f072daae273d6299 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 02:14:22 -0600 Subject: [PATCH 25/30] docs: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da6ccbd..f5c851f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@

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. + Iterate, filter, transform, sort, reduce, group, and debug data with zero dependencies - familiar to Go developers, pleasant to use everywhere.

## Features From bb5eace07b862db3ebf7625127a3f8f7665e7a63 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 02:16:46 -0600 Subject: [PATCH 26/30] docs: readme --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index f5c851f..256aba9 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,6 @@ - **Developer-friendly debug helpers** (`Dump()`, `Dd()`, `DumpStr()`) - **Works with any Go type**, including structs, pointers, and deeply nested composites -## Why Collection? - -This library is ideal when: -- you write multi-step slice pipelines -- allocation behavior matters -- you want fluent APIs without hidden costs - ## Fluent Chaining Many methods return the collection itself, allowing for fluent method chaining. From 5a908ffb125f40eab66a26920386a79146506766 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 02:53:07 -0600 Subject: [PATCH 27/30] chore: mutability guarantees test coverage --- README.md | 4 +- mutability_guarantees_test.go | 223 ++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 mutability_guarantees_test.go diff --git a/README.md b/README.md index 256aba9..4ccce3b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Latest tag - Tests + Tests Go Report Card

@@ -946,7 +946,7 @@ collection.Dump(odds.Items()) ### New · immutable · chainable -New creates a new Collection that borrows the provided slice without copying. +New creates a new Collection from the provided slice and borrows it. ### NewNumeric · immutable · chainable diff --git a/mutability_guarantees_test.go b/mutability_guarantees_test.go new file mode 100644 index 0000000..06c0a59 --- /dev/null +++ b/mutability_guarantees_test.go @@ -0,0 +1,223 @@ +package collection + +import ( + "reflect" + "testing" +) + +func TestMutators_PreserveNilSlice(t *testing.T) { + cases := []struct { + name string + fn func(*Collection[int]) + }{ + { + name: "Map", + fn: func(c *Collection[int]) { + c.Map(func(v int) int { return v * 2 }) + }, + }, + { + name: "Filter", + fn: func(c *Collection[int]) { + c.Filter(func(v int) bool { return v%2 == 0 }) + }, + }, + { + name: "Sort", + fn: func(c *Collection[int]) { + c.Sort(func(a, b int) bool { return a < b }) + }, + }, + { + name: "Reverse", + fn: func(c *Collection[int]) { + c.Reverse() + }, + }, + { + name: "Shuffle", + fn: func(c *Collection[int]) { + c.Shuffle() + }, + }, + { + name: "Transform", + fn: func(c *Collection[int]) { + c.Transform(func(v int) int { return v + 1 }) + }, + }, + { + name: "Pop", + fn: func(c *Collection[int]) { + _, _ = c.Pop() + }, + }, + { + name: "PopN", + fn: func(c *Collection[int]) { + _ = c.PopN(2) + }, + }, + { + name: "ConcatEmpty", + fn: func(c *Collection[int]) { + c.Concat([]int{}) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c := New([]int(nil)) + tc.fn(c) + + if c.Items() != nil { + t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) + } + }) + } +} + +func TestMap_WritesThroughSourceSlice(t *testing.T) { + items := []int{1, 2, 3} + c := New(items) + + c.Map(func(v int) int { return v * 2 }) + + want := []int{2, 4, 6} + if !reflect.DeepEqual(items, want) { + t.Fatalf("expected source slice %v, got %v", want, items) + } +} + +func TestFilter_WritesThroughSourceSlice(t *testing.T) { + items := []int{1, 2, 3, 4} + c := New(items) + + c.Filter(func(v int) bool { return v%2 == 0 }) + + want := []int{2, 4} + if !reflect.DeepEqual(items[:len(want)], want) { + t.Fatalf("expected source prefix %v, got %v", want, items[:len(want)]) + } + + if len(c.Items()) != len(want) { + t.Fatalf("expected filtered length %d, got %d", len(want), len(c.Items())) + } +} + +func TestSort_WritesThroughSourceSlice(t *testing.T) { + items := []int{3, 1, 2} + c := New(items) + + c.Sort(func(a, b int) bool { return a < b }) + + want := []int{1, 2, 3} + if !reflect.DeepEqual(items, want) { + t.Fatalf("expected source slice %v, got %v", want, items) + } +} + +func TestTransform_WritesThroughSourceSlice(t *testing.T) { + items := []int{1, 2, 3} + c := New(items) + + c.Transform(func(v int) int { return v + 10 }) + + want := []int{11, 12, 13} + if !reflect.DeepEqual(items, want) { + t.Fatalf("expected source slice %v, got %v", want, items) + } +} + +func TestReverse_WritesThroughSourceSlice(t *testing.T) { + items := []int{1, 2, 3} + c := New(items) + + c.Reverse() + + want := []int{3, 2, 1} + if !reflect.DeepEqual(items, want) { + t.Fatalf("expected source slice %v, got %v", want, items) + } +} + +func TestMutators_LengthGuarantees(t *testing.T) { + cases := []struct { + name string + input []int + fn func(*Collection[int]) any + wantLen int + }{ + { + name: "Map", + input: []int{1, 2, 3}, + fn: func(c *Collection[int]) any { return c.Map(func(v int) int { return v * 2 }) }, + wantLen: 3, + }, + { + name: "Filter", + input: []int{1, 2, 3, 4}, + fn: func(c *Collection[int]) any { return c.Filter(func(v int) bool { return v%2 == 0 }) }, + wantLen: 2, + }, + { + name: "Sort", + input: []int{3, 1, 2}, + fn: func(c *Collection[int]) any { return c.Sort(func(a, b int) bool { return a < b }) }, + wantLen: 3, + }, + { + name: "Transform", + input: []int{1, 2, 3}, + fn: func(c *Collection[int]) any { c.Transform(func(v int) int { return v + 1 }); return nil }, + wantLen: 3, + }, + { + name: "Reverse", + input: []int{1, 2, 3}, + fn: func(c *Collection[int]) any { return c.Reverse() }, + wantLen: 3, + }, + { + name: "Shuffle", + input: []int{1, 2, 3, 4}, + fn: func(c *Collection[int]) any { return c.Shuffle() }, + wantLen: 4, + }, + { + name: "Pop", + input: []int{1, 2, 3}, + fn: func(c *Collection[int]) any { _, _ = c.Pop(); return nil }, + wantLen: 2, + }, + { + name: "PopN", + input: []int{1, 2, 3, 4}, + fn: func(c *Collection[int]) any { _ = c.PopN(2); return nil }, + wantLen: 2, + }, + { + name: "Prepend", + input: []int{3, 4}, + fn: func(c *Collection[int]) any { return c.Prepend(1, 2) }, + wantLen: 4, + }, + { + name: "Concat", + input: []int{1, 2}, + fn: func(c *Collection[int]) any { return c.Concat([]int{3, 4, 5}) }, + wantLen: 5, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c := New(append([]int(nil), tc.input...)) + tc.fn(c) + if len(c.items) != tc.wantLen { + t.Fatalf("expected len %d, got %d", tc.wantLen, len(c.items)) + } + }) + } +} From e55a61aecd121fccf2fe7c5b2c86a3d4f0361458 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 13:24:11 -0600 Subject: [PATCH 28/30] chore: test colocation --- concat_test.go | 21 ++++ filter_test.go | 26 ++++ map_test.go | 32 +++++ mutability_guarantees_test.go | 223 ---------------------------------- pop_test.go | 28 +++++ prepend_test.go | 24 ++++ reverse_test.go | 10 ++ shuffle_test.go | 10 ++ sort_test.go | 32 +++++ transform_test.go | 32 +++++ 10 files changed, 215 insertions(+), 223 deletions(-) delete mode 100644 mutability_guarantees_test.go diff --git a/concat_test.go b/concat_test.go index 62936b1..3070754 100644 --- a/concat_test.go +++ b/concat_test.go @@ -116,3 +116,24 @@ func TestConcat_NoAllocationWhenCapacityAllows(t *testing.T) { t.Fatalf("expected %v, got %v", expected, c.items) } } + +func TestConcat_PreservesNilSliceWhenEmptyValues(t *testing.T) { + c := New([]int(nil)) + + c.Concat([]int{}) + + if c.Items() != nil { + t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) + } +} + +func TestConcat_NilSliceWithValues(t *testing.T) { + c := New([]int(nil)) + + c.Concat([]int{1, 2}) + + expected := []int{1, 2} + if !reflect.DeepEqual(c.Items(), expected) { + t.Fatalf("expected %v, got %v", expected, c.Items()) + } +} diff --git a/filter_test.go b/filter_test.go index ea9901f..0e04b54 100644 --- a/filter_test.go +++ b/filter_test.go @@ -90,3 +90,29 @@ func TestFilter_Chaining(t *testing.T) { t.Fatalf("expected %v, got %v", expected, result.items) } } + +func TestFilter_PreservesNilSlice(t *testing.T) { + c := New([]int(nil)) + + c.Filter(func(v int) bool { return v%2 == 0 }) + + if c.Items() != nil { + t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) + } +} + +func TestFilter_WritesThroughSourceSlice(t *testing.T) { + items := []int{1, 2, 3, 4} + c := New(items) + + c.Filter(func(v int) bool { return v%2 == 0 }) + + want := []int{2, 4} + if !reflect.DeepEqual(items[:len(want)], want) { + t.Fatalf("expected source prefix %v, got %v", want, items[:len(want)]) + } + + if len(c.Items()) != len(want) { + t.Fatalf("expected filtered length %d, got %d", len(want), len(c.Items())) + } +} diff --git a/map_test.go b/map_test.go index 76dd367..4d6365a 100644 --- a/map_test.go +++ b/map_test.go @@ -76,3 +76,35 @@ func TestMap_Empty(t *testing.T) { t.Fatalf("Map should return the same collection") } } + +func TestMap_PreservesNilSlice(t *testing.T) { + c := New([]int(nil)) + + c.Map(func(v int) int { return v * 2 }) + + if c.Items() != nil { + t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) + } +} + +func TestMap_WritesThroughSourceSlice(t *testing.T) { + items := []int{1, 2, 3} + c := New(items) + + c.Map(func(v int) int { return v * 2 }) + + want := []int{2, 4, 6} + if !reflect.DeepEqual(items, want) { + t.Fatalf("expected source slice %v, got %v", want, items) + } +} + +func TestMap_LengthUnchanged(t *testing.T) { + c := New([]int{1, 2, 3}) + + c.Map(func(v int) int { return v + 1 }) + + if len(c.Items()) != 3 { + t.Fatalf("expected length 3, got %d", len(c.Items())) + } +} diff --git a/mutability_guarantees_test.go b/mutability_guarantees_test.go deleted file mode 100644 index 06c0a59..0000000 --- a/mutability_guarantees_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package collection - -import ( - "reflect" - "testing" -) - -func TestMutators_PreserveNilSlice(t *testing.T) { - cases := []struct { - name string - fn func(*Collection[int]) - }{ - { - name: "Map", - fn: func(c *Collection[int]) { - c.Map(func(v int) int { return v * 2 }) - }, - }, - { - name: "Filter", - fn: func(c *Collection[int]) { - c.Filter(func(v int) bool { return v%2 == 0 }) - }, - }, - { - name: "Sort", - fn: func(c *Collection[int]) { - c.Sort(func(a, b int) bool { return a < b }) - }, - }, - { - name: "Reverse", - fn: func(c *Collection[int]) { - c.Reverse() - }, - }, - { - name: "Shuffle", - fn: func(c *Collection[int]) { - c.Shuffle() - }, - }, - { - name: "Transform", - fn: func(c *Collection[int]) { - c.Transform(func(v int) int { return v + 1 }) - }, - }, - { - name: "Pop", - fn: func(c *Collection[int]) { - _, _ = c.Pop() - }, - }, - { - name: "PopN", - fn: func(c *Collection[int]) { - _ = c.PopN(2) - }, - }, - { - name: "ConcatEmpty", - fn: func(c *Collection[int]) { - c.Concat([]int{}) - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - c := New([]int(nil)) - tc.fn(c) - - if c.Items() != nil { - t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) - } - }) - } -} - -func TestMap_WritesThroughSourceSlice(t *testing.T) { - items := []int{1, 2, 3} - c := New(items) - - c.Map(func(v int) int { return v * 2 }) - - want := []int{2, 4, 6} - if !reflect.DeepEqual(items, want) { - t.Fatalf("expected source slice %v, got %v", want, items) - } -} - -func TestFilter_WritesThroughSourceSlice(t *testing.T) { - items := []int{1, 2, 3, 4} - c := New(items) - - c.Filter(func(v int) bool { return v%2 == 0 }) - - want := []int{2, 4} - if !reflect.DeepEqual(items[:len(want)], want) { - t.Fatalf("expected source prefix %v, got %v", want, items[:len(want)]) - } - - if len(c.Items()) != len(want) { - t.Fatalf("expected filtered length %d, got %d", len(want), len(c.Items())) - } -} - -func TestSort_WritesThroughSourceSlice(t *testing.T) { - items := []int{3, 1, 2} - c := New(items) - - c.Sort(func(a, b int) bool { return a < b }) - - want := []int{1, 2, 3} - if !reflect.DeepEqual(items, want) { - t.Fatalf("expected source slice %v, got %v", want, items) - } -} - -func TestTransform_WritesThroughSourceSlice(t *testing.T) { - items := []int{1, 2, 3} - c := New(items) - - c.Transform(func(v int) int { return v + 10 }) - - want := []int{11, 12, 13} - if !reflect.DeepEqual(items, want) { - t.Fatalf("expected source slice %v, got %v", want, items) - } -} - -func TestReverse_WritesThroughSourceSlice(t *testing.T) { - items := []int{1, 2, 3} - c := New(items) - - c.Reverse() - - want := []int{3, 2, 1} - if !reflect.DeepEqual(items, want) { - t.Fatalf("expected source slice %v, got %v", want, items) - } -} - -func TestMutators_LengthGuarantees(t *testing.T) { - cases := []struct { - name string - input []int - fn func(*Collection[int]) any - wantLen int - }{ - { - name: "Map", - input: []int{1, 2, 3}, - fn: func(c *Collection[int]) any { return c.Map(func(v int) int { return v * 2 }) }, - wantLen: 3, - }, - { - name: "Filter", - input: []int{1, 2, 3, 4}, - fn: func(c *Collection[int]) any { return c.Filter(func(v int) bool { return v%2 == 0 }) }, - wantLen: 2, - }, - { - name: "Sort", - input: []int{3, 1, 2}, - fn: func(c *Collection[int]) any { return c.Sort(func(a, b int) bool { return a < b }) }, - wantLen: 3, - }, - { - name: "Transform", - input: []int{1, 2, 3}, - fn: func(c *Collection[int]) any { c.Transform(func(v int) int { return v + 1 }); return nil }, - wantLen: 3, - }, - { - name: "Reverse", - input: []int{1, 2, 3}, - fn: func(c *Collection[int]) any { return c.Reverse() }, - wantLen: 3, - }, - { - name: "Shuffle", - input: []int{1, 2, 3, 4}, - fn: func(c *Collection[int]) any { return c.Shuffle() }, - wantLen: 4, - }, - { - name: "Pop", - input: []int{1, 2, 3}, - fn: func(c *Collection[int]) any { _, _ = c.Pop(); return nil }, - wantLen: 2, - }, - { - name: "PopN", - input: []int{1, 2, 3, 4}, - fn: func(c *Collection[int]) any { _ = c.PopN(2); return nil }, - wantLen: 2, - }, - { - name: "Prepend", - input: []int{3, 4}, - fn: func(c *Collection[int]) any { return c.Prepend(1, 2) }, - wantLen: 4, - }, - { - name: "Concat", - input: []int{1, 2}, - fn: func(c *Collection[int]) any { return c.Concat([]int{3, 4, 5}) }, - wantLen: 5, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - c := New(append([]int(nil), tc.input...)) - tc.fn(c) - if len(c.items) != tc.wantLen { - t.Fatalf("expected len %d, got %d", tc.wantLen, len(c.items)) - } - }) - } -} diff --git a/pop_test.go b/pop_test.go index e22855a..04a4295 100644 --- a/pop_test.go +++ b/pop_test.go @@ -94,3 +94,31 @@ func TestPopN_ZeroOrNegative(t *testing.T) { t.Fatalf("PopN(-n) should not modify original") } } + +func TestPop_PreservesNilSlice(t *testing.T) { + c := New([]int(nil)) + + v, ok := c.Pop() + + if v != 0 || ok { + t.Fatalf("Pop() on nil should return zero-value and ok=false") + } + + if c.Items() != nil { + t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) + } +} + +func TestPopN_PreservesNilSlice(t *testing.T) { + c := New([]int(nil)) + + popped := c.PopN(2) + + if popped != nil { + t.Fatalf("PopN on nil should return nil") + } + + if c.Items() != nil { + t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) + } +} diff --git a/prepend_test.go b/prepend_test.go index f878334..3e74c7a 100644 --- a/prepend_test.go +++ b/prepend_test.go @@ -98,3 +98,27 @@ func TestPrepend_DoesNotMutateSourceSlice(t *testing.T) { t.Fatalf("expected %v, got %v", expected, c.items) } } + +func TestPrepend_NilSliceWithValues(t *testing.T) { + c := New([]int(nil)) + + c.Prepend(1, 2) + + expected := []int{1, 2} + if !reflect.DeepEqual(c.Items(), expected) { + t.Fatalf("expected %v, got %v", expected, c.Items()) + } +} + +func TestPrepend_NilSliceNoValuesBecomesEmpty(t *testing.T) { + c := New([]int(nil)) + + c.Prepend() + + if c.Items() == nil { + t.Fatalf("expected empty slice, got nil") + } + if len(c.Items()) != 0 { + t.Fatalf("expected empty slice, got %v", c.Items()) + } +} diff --git a/reverse_test.go b/reverse_test.go index 98eb50e..12ac3c4 100644 --- a/reverse_test.go +++ b/reverse_test.go @@ -96,3 +96,13 @@ func TestReverse_MutatesInPlace(t *testing.T) { t.Fatalf("expected underlying slice to be mutated, got %v", items) } } + +func TestReverse_PreservesNilSlice(t *testing.T) { + c := New([]int(nil)) + + c.Reverse() + + if c.Items() != nil { + t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) + } +} diff --git a/shuffle_test.go b/shuffle_test.go index aa989fe..843a30f 100644 --- a/shuffle_test.go +++ b/shuffle_test.go @@ -119,3 +119,13 @@ func TestShuffle_MutatesInPlace(t *testing.T) { } }) } + +func TestShuffle_PreservesNilSlice(t *testing.T) { + c := New([]int(nil)) + + c.Shuffle() + + if c.Items() != nil { + t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) + } +} diff --git a/sort_test.go b/sort_test.go index a359a34..0ed2b00 100644 --- a/sort_test.go +++ b/sort_test.go @@ -111,3 +111,35 @@ func TestSort_SingleElement(t *testing.T) { t.Fatalf("expected %v, got %v", expected, sorted.items) } } + +func TestSort_PreservesNilSlice(t *testing.T) { + c := New([]int(nil)) + + c.Sort(func(a, b int) bool { return a < b }) + + if c.Items() != nil { + t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) + } +} + +func TestSort_WritesThroughSourceSlice(t *testing.T) { + items := []int{3, 1, 2} + c := New(items) + + c.Sort(func(a, b int) bool { return a < b }) + + want := []int{1, 2, 3} + if !reflect.DeepEqual(items, want) { + t.Fatalf("expected source slice %v, got %v", want, items) + } +} + +func TestSort_LengthUnchanged(t *testing.T) { + c := New([]int{3, 1, 2}) + + c.Sort(func(a, b int) bool { return a < b }) + + if len(c.Items()) != 3 { + t.Fatalf("expected length 3, got %d", len(c.Items())) + } +} diff --git a/transform_test.go b/transform_test.go index 11614d5..4ced533 100644 --- a/transform_test.go +++ b/transform_test.go @@ -92,3 +92,35 @@ func TestTransform_ChainingCompatibility(t *testing.T) { t.Fatalf("expected %v, got %v", expected, out.items) } } + +func TestTransform_PreservesNilSlice(t *testing.T) { + c := New([]int(nil)) + + c.Transform(func(v int) int { return v + 1 }) + + if c.Items() != nil { + t.Fatalf("expected nil slice to remain nil, got %v", c.Items()) + } +} + +func TestTransform_WritesThroughSourceSlice(t *testing.T) { + items := []int{1, 2, 3} + c := New(items) + + c.Transform(func(v int) int { return v + 10 }) + + want := []int{11, 12, 13} + if !reflect.DeepEqual(items, want) { + t.Fatalf("expected source slice %v, got %v", want, items) + } +} + +func TestTransform_LengthUnchanged(t *testing.T) { + c := New([]int{1, 2, 3}) + + c.Transform(func(v int) int { return v + 1 }) + + if len(c.Items()) != 3 { + t.Fatalf("expected length 3, got %d", len(c.Items())) + } +} From fe2911f45d5f82513c1f122f6b26064b974d6d62 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 13:49:02 -0600 Subject: [PATCH 29/30] chore: remove benchInner --- BENCHMARKS.md | 132 +++++++++---------- README.md | 25 ++-- docs/bench/main.go | 317 +++++++++++++-------------------------------- 3 files changed, 168 insertions(+), 306 deletions(-) diff --git a/BENCHMARKS.md b/BENCHMARKS.md index ffc2965..0c3a9e8 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -4,74 +4,74 @@ Raw results for `collection.New` (borrowed) vs `lo`. | Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 251ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Any** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Chunk** | 141ns / 1.1µs | **7.49x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | -| **Contains** | 249ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **CountBy** | 8.2µs / 8.0µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **CountByValue** | 8.2µs / 8.1µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | -| **Difference** | 19.1µs / 44.2µs | **2.31x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | -| **Each** | 253ns / 231ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Filter** | 644ns / 1.1µs | **1.64x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **First** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **FirstWhere** | 249ns / 234ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **GroupBySlice** | 8.0µs / 8.4µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | -| **IndexWhere** | 250ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Intersect** | 11.0µs / 10.7µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | -| **Last** | 11ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Map** | 358ns / 819ns | **2.29x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Max** | 254ns / 231ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Min** | 255ns / 236ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **None** | 248ns / 234ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Pipeline F→M→T→R** | 504ns / 1.3µs | **2.53x** | 48B / 12.3KB | **256.00x less** | 2 / 2 | -| **Reduce (sum)** | 253ns / 236ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Reverse** | 227ns / 235ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | -| **Shuffle** | 3.5µs / 5.5µs | **1.56x** | 24B / 0B | ∞x more | 1 / 0 | -| **Skip** | 11ns / 726ns | **65.27x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **SkipLast** | 11ns / 730ns | **64.89x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | -| **Sum** | 255ns / 236ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | -| **Take** | 22ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | -| **ToMap** | 7.6µs / 8.0µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | -| **Union** | 17.2µs / 17.9µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | -| **Unique** | 6.4µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | -| **UniqueBy** | 6.8µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | -| **Zip** | 1.5µs / 3.2µs | **2.22x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | -| **ZipWith** | 1.0µs / 3.0µs | **2.87x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **All** | 248ns / 232ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Any** | 248ns / 235ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Chunk** | 138ns / 1.1µs | **7.64x** | 1.3KB / 9.3KB | **7.12x less** | 2 / 51 | +| **Contains** | 250ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **CountBy** | 8.6µs / 8.4µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **CountByValue** | 8.3µs / 8.3µs | ≈ | 9.4KB / 9.4KB | ≈ | 12 / 11 | +| **Difference** | 19.5µs / 45.6µs | **2.33x** | 82.2KB / 108.8KB | **1.32x less** | 14 / 43 | +| **Each** | 256ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Filter** | 734ns / 1.0µs | **1.41x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **First** | 10ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **FirstWhere** | 251ns / 244ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **GroupBySlice** | 8.1µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 84 / 83 | +| **IndexWhere** | 247ns / 233ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Intersect** | 11.1µs / 10.8µs | ≈ | 11.5KB / 11.4KB | ≈ | 22 / 19 | +| **Last** | 10ns / <1ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Map** | 358ns / 790ns | **2.21x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Max** | 263ns / 230ns | 0.87x | 32B / 0B | ∞x more | 2 / 0 | +| **Min** | 253ns / 230ns | ≈ | 32B / 0B | ∞x more | 2 / 0 | +| **None** | 248ns / 231ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Pipeline F→M→T→R** | 690ns / 1.2µs | **1.79x** | 48B / 12.3KB | **256.00x less** | 2 / 2 | +| **Reduce (sum)** | 262ns / 233ns | 0.89x | 24B / 0B | ∞x more | 1 / 0 | +| **Reverse** | 230ns / 230ns | ≈ | 24B / 0B | ∞x more | 1 / 0 | +| **Shuffle** | 3.6µs / 5.6µs | **1.57x** | 24B / 0B | ∞x more | 1 / 0 | +| **Skip** | 10ns / 714ns | **71.40x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **SkipLast** | 10ns / 719ns | **71.90x** | 24B / 8.2KB | **341.33x less** | 1 / 1 | +| **Sum** | 267ns / 231ns | 0.87x | 32B / 0B | ∞x more | 2 / 0 | +| **Take** | 21ns / <1ns | ≈ | 48B / 0B | ∞x more | 2 / 0 | +| **ToMap** | 7.8µs / 7.9µs | ≈ | 37.0KB / 37.0KB | ≈ | 6 / 6 | +| **Union** | 17.4µs / 17.9µs | ≈ | 90.3KB / 90.3KB | ≈ | 13 / 10 | +| **Unique** | 6.5µs / 6.2µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 | +| **UniqueBy** | 6.6µs / 6.4µs | ≈ | 45.2KB / 45.1KB | ≈ | 8 / 6 | +| **Zip** | 1.4µs / 3.3µs | **2.35x** | 16.4KB / 16.4KB | ≈ | 3 / 1 | +| **ZipWith** | 1.0µs / 3.2µs | **3.15x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | Raw results for `collection.New().Clone()` (explicit copy) vs `lo`. | Op | ns/op (vs lo) | × (faster) | bytes/op (vs lo) | × (less memory) | allocs/op (vs lo) | |---:|----------------|:--:|------------------|:--:|--------------------| -| **All** | 916ns / 234ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Any** | 920ns / 235ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Chunk** | 862ns / 1.1µs | **1.26x** | 9.5KB / 9.3KB | ≈ | 4 / 51 | -| **Contains** | 924ns / 234ns | 0.25x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **CountBy** | 8.9µs / 8.6µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | -| **CountByValue** | 9.0µs / 15.2µs | **1.68x** | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | -| **Difference** | 20.6µs / 44.8µs | **2.17x** | 98.6KB / 108.8KB | **1.10x less** | 18 / 43 | -| **Each** | 913ns / 232ns | 0.25x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Filter** | 1.2µs / 1.0µs | 0.89x | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **First** | 737ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **FirstWhere** | 934ns / 236ns | 0.25x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **GroupBySlice** | 8.7µs / 8.7µs | ≈ | 29.3KB / 21.0KB | 0.72x more | 86 / 83 | -| **IndexWhere** | 925ns / 234ns | 0.25x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Intersect** | 12.2µs / 10.9µs | 0.90x | 27.9KB / 11.4KB | 0.41x more | 26 / 19 | -| **Last** | 740ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Map** | 958ns / 816ns | 0.85x | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **Max** | 930ns / 231ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | -| **Min** | 925ns / 236ns | 0.26x | 8.3KB / 0B | ∞x more | 5 / 0 | -| **None** | 916ns / 236ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Pipeline F→M→T→R** | 1.1µs / 1.3µs | **1.21x** | 8.3KB / 12.3KB | **1.49x less** | 4 / 2 | -| **Reduce (sum)** | 911ns / 236ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Reverse** | 841ns / 240ns | 0.29x | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Shuffle** | 4.2µs / 5.7µs | **1.34x** | 8.2KB / 0B | ∞x more | 3 / 0 | -| **Skip** | 771ns / 982ns | **1.27x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **SkipLast** | 2.2µs / 736ns | 0.33x | 8.2KB / 8.2KB | ≈ | 3 / 1 | -| **Sum** | 918ns / 236ns | 0.26x | 8.3KB / 0B | ∞x more | 5 / 0 | -| **Take** | 746ns / <1ns | 0.00x | 8.3KB / 0B | ∞x more | 4 / 0 | -| **ToMap** | 8.3µs / 8.0µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 8 / 6 | -| **Union** | 18.6µs / 18.1µs | ≈ | 106.8KB / 90.3KB | 0.85x more | 17 / 10 | -| **Unique** | 7.1µs / 6.4µs | ≈ | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | -| **UniqueBy** | 7.5µs / 6.5µs | 0.86x | 53.4KB / 45.1KB | 0.85x more | 10 / 6 | -| **Zip** | 2.8µs / 3.3µs | **1.17x** | 32.9KB / 16.4KB | 0.50x more | 7 / 1 | -| **ZipWith** | 2.4µs / 3.0µs | **1.26x** | 24.7KB / 8.2KB | 0.33x more | 7 / 1 | +| **All** | 921ns / 245ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Any** | 908ns / 245ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Chunk** | 855ns / 1.1µs | **1.28x** | 9.5KB / 9.3KB | ≈ | 4 / 51 | +| **Contains** | 912ns / 244ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **CountBy** | 9.2µs / 8.6µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | +| **CountByValue** | 9.2µs / 8.6µs | ≈ | 17.6KB / 9.4KB | 0.53x more | 14 / 11 | +| **Difference** | 21.1µs / 45.9µs | **2.17x** | 98.6KB / 108.8KB | **1.10x less** | 18 / 43 | +| **Each** | 887ns / 235ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Filter** | 1.2µs / 1.0µs | 0.85x | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **First** | 707ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **FirstWhere** | 903ns / 247ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **GroupBySlice** | 9.4µs / 8.7µs | ≈ | 29.3KB / 21.0KB | 0.72x more | 86 / 83 | +| **IndexWhere** | 887ns / 236ns | 0.27x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Intersect** | 12.6µs / 11.2µs | 0.89x | 27.9KB / 11.4KB | 0.41x more | 26 / 19 | +| **Last** | 730ns / <1ns | 0.00x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Map** | 903ns / 782ns | 0.87x | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **Max** | 928ns / 236ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **Min** | 931ns / 235ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **None** | 882ns / 232ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Pipeline F→M→T→R** | 1.2µs / 1.2µs | ≈ | 8.3KB / 12.3KB | **1.49x less** | 4 / 2 | +| **Reduce (sum)** | 887ns / 231ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Reverse** | 904ns / 235ns | 0.26x | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Shuffle** | 4.1µs / 5.7µs | **1.39x** | 8.2KB / 0B | ∞x more | 3 / 0 | +| **Skip** | 730ns / 955ns | **1.31x** | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **SkipLast** | 768ns / 772ns | ≈ | 8.2KB / 8.2KB | ≈ | 3 / 1 | +| **Sum** | 944ns / 239ns | 0.25x | 8.3KB / 0B | ∞x more | 5 / 0 | +| **Take** | 727ns / <1ns | 0.00x | 8.3KB / 0B | ∞x more | 4 / 0 | +| **ToMap** | 8.4µs / 8.1µs | ≈ | 45.2KB / 37.0KB | 0.82x more | 8 / 6 | +| **Union** | 19.6µs / 18.3µs | ≈ | 106.8KB / 90.3KB | 0.85x more | 17 / 10 | +| **Unique** | 7.4µs / 7.5µs | ≈ | 53.4KB / 45.1KB | 0.85x more | 9 / 6 | +| **UniqueBy** | 10.4µs / 6.6µs | 0.64x | 53.4KB / 45.1KB | 0.85x more | 10 / 6 | +| **Zip** | 2.9µs / 3.3µs | **1.15x** | 32.9KB / 16.4KB | 0.50x more | 7 / 1 | +| **ZipWith** | 2.5µs / 3.1µs | **1.27x** | 24.7KB / 8.2KB | 0.33x more | 7 / 1 | diff --git a/README.md b/README.md index 4ccce3b..f7d5a62 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Latest tag - Tests + Tests Go Report Card

@@ -29,7 +29,7 @@ - **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** - views for slice selections; in-place ops reuse backing when possible +- **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 @@ -114,17 +114,17 @@ Full raw tables: see `BENCHMARKS.md`. | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Chunk** | **7.49x** | -8.0KB | -49 | +| **Chunk** | **7.64x** | -8.0KB | -49 | | **Take** | ≈ | +48B | +2 | -| **Skip** | **65.27x** | -8.2KB | ≈ | -| **SkipLast** | **64.89x** | -8.2KB | ≈ | -| **Zip** | **2.22x** | +48B | +2 | -| **ZipWith** | **2.87x** | +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.31x** | -26.7KB | -29 | +| **Difference** | **2.33x** | -26.7KB | -29 | | **GroupBySlice** | ≈ | +24B | +1 | | **CountBy** | ≈ | +24B | +1 | | **CountByValue** | ≈ | +24B | +1 | @@ -134,16 +134,16 @@ Full raw tables: see `BENCHMARKS.md`. | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Pipeline F→M→T→R** | **2.53x** | -12.2KB | ≈ | +| **Pipeline F→M→T→R** | **1.79x** | -12.2KB | ≈ | #### Mutating ops | Op | Speed vs lo | Memory | Allocs | |---:|:-----------:|:------:|:------:| -| **Map** | **2.29x** | -8.2KB | ≈ | -| **Filter** | **1.64x** | -8.2KB | ≈ | +| **Map** | **2.21x** | -8.2KB | ≈ | +| **Filter** | **1.41x** | -8.2KB | ≈ | | **Reverse** | ≈ | +24B | +1 | -| **Shuffle** | **1.56x** | +24B | +1 | +| **Shuffle** | **1.57x** | +24B | +1 | ## How to read the benchmarks @@ -234,6 +234,7 @@ No hidden copies. No surprises. - 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 diff --git a/docs/bench/main.go b/docs/bench/main.go index 3297c45..4aea988 100644 --- a/docs/bench/main.go +++ b/docs/bench/main.go @@ -20,8 +20,6 @@ const ( benchEnd = "" hotPathIters = 10_000 - - benchInner = 8 ) type benchResult struct { @@ -172,11 +170,6 @@ func measure(name, impl string, fn func(*testing.B)) benchResult { nsPerOp := float64(res.NsPerOp()) bytesPerOp := res.AllocedBytesPerOp() allocsPerOp := res.AllocsPerOp() - if benchInner > 1 { - nsPerOp = nsPerOp / float64(benchInner) - bytesPerOp = bytesPerOp / int64(benchInner) - allocsPerOp = allocsPerOp / int64(benchInner) - } return benchResult{ name: name, @@ -239,684 +232,552 @@ func init() { func benchPipelineCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - input := collectionInputForMutating(benchInts) - _ = ctorInts(input). - Filter(func(v int) bool { return v%2 == 0 }). - Map(func(v int) int { return v * v }). - Take(benchPipelineLen). - Reduce(0, func(acc, v int) int { return acc + v }) + input := collectionInputForMutating(benchInts) + _ = ctorInts(input). + Filter(func(v int) bool { return v%2 == 0 }). + Map(func(v int) int { return v * v }). + Take(benchPipelineLen). + Reduce(0, func(acc, v int) int { return acc + v }) - } } } func benchPipelineLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - input := benchInts + input := benchInts - out := lo.Filter(input, func(v int, _ int) bool { return v%2 == 0 }) - out2 := lo.Map(out, func(v int, _ int) int { return v * v }) - out3 := lo.Subset(out2, 0, benchPipelineLen) - _ = lo.Reduce(out3, func(acc int, v int, _ int) int { return acc + v }, 0) + out := lo.Filter(input, func(v int, _ int) bool { return v%2 == 0 }) + out2 := lo.Map(out, func(v int, _ int) int { return v * v }) + out3 := lo.Subset(out2, 0, benchPipelineLen) + _ = lo.Reduce(out3, func(acc int, v int, _ int) int { return acc + v }, 0) - } } } func benchAllCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = ctorInts(benchInts).All(func(v int) bool { return v < benchSize+1 }) + _ = ctorInts(benchInts).All(func(v int) bool { return v < benchSize+1 }) - } } } func benchAllLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.EveryBy(benchInts, func(v int) bool { return v < benchSize+1 }) + _ = lo.EveryBy(benchInts, func(v int) bool { return v < benchSize+1 }) - } } } func benchAnyCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = ctorInts(benchInts).Any(func(v int) bool { return v == benchSize-1 }) + _ = ctorInts(benchInts).Any(func(v int) bool { return v == benchSize-1 }) - } } } func benchAnyLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.SomeBy(benchInts, func(v int) bool { return v == benchSize-1 }) + _ = lo.SomeBy(benchInts, func(v int) bool { return v == benchSize-1 }) - } } } func benchNoneCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = ctorInts(benchInts).None(func(v int) bool { return v < 0 }) + _ = ctorInts(benchInts).None(func(v int) bool { return v < 0 }) - } } } func benchNoneLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.NoneBy(benchInts, func(v int) bool { return v < 0 }) + _ = lo.NoneBy(benchInts, func(v int) bool { return v < 0 }) - } } } func benchFirstCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _ = ctorInts(benchInts).First() + _, _ = ctorInts(benchInts).First() - } } } func benchFirstLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _ = lo.First(benchInts) + _, _ = lo.First(benchInts) - } } } func benchLastCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _ = ctorInts(benchInts).Last() + _, _ = ctorInts(benchInts).Last() - } } } func benchLastLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _ = lo.Last(benchInts) + _, _ = lo.Last(benchInts) - } } } func benchIndexWhereCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _ = ctorInts(benchInts).IndexWhere(func(v int) bool { return v == benchSize-1 }) + _, _ = ctorInts(benchInts).IndexWhere(func(v int) bool { return v == benchSize-1 }) - } } } func benchIndexWhereLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _, _ = lo.FindIndexOf(benchInts, func(v int) bool { return v == benchSize-1 }) + _, _, _ = lo.FindIndexOf(benchInts, func(v int) bool { return v == benchSize-1 }) - } } } func benchEachCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - sum := 0 - ctorInts(benchInts).Each(func(v int) { sum += v }) + sum := 0 + ctorInts(benchInts).Each(func(v int) { sum += v }) - } } } func benchEachLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - sum := 0 - lo.ForEach(benchInts, func(v int, _ int) { sum += v }) + sum := 0 + lo.ForEach(benchInts, func(v int, _ int) { sum += v }) - } } } func benchMapCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - input := collectionInputForMutating(benchInts) - _ = ctorInts(input).Map(func(v int) int { return v * 3 }) + input := collectionInputForMutating(benchInts) + _ = ctorInts(input).Map(func(v int) int { return v * 3 }) - } } } func benchMapLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - input := benchInts - _ = lo.Map(input, func(v int, _ int) int { return v * 3 }) + input := benchInts + _ = lo.Map(input, func(v int, _ int) int { return v * 3 }) - } } } func benchReduceCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = ctorInts(benchInts).Reduce(0, func(acc, v int) int { return acc + v }) + _ = ctorInts(benchInts).Reduce(0, func(acc, v int) int { return acc + v }) - } } } func benchReduceLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Reduce(benchInts, func(acc int, v int, _ int) int { return acc + v }, 0) + _ = lo.Reduce(benchInts, func(acc int, v int, _ int) int { return acc + v }, 0) - } } } func benchFilterCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - input := collectionInputForMutating(benchInts) - _ = ctorInts(input).Filter(func(v int) bool { return v%3 == 0 }) + input := collectionInputForMutating(benchInts) + _ = ctorInts(input).Filter(func(v int) bool { return v%3 == 0 }) - } } } func benchFilterLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - input := benchInts - _ = lo.Filter(input, func(v int, _ int) bool { return v%3 == 0 }) + input := benchInts + _ = lo.Filter(input, func(v int, _ int) bool { return v%3 == 0 }) - } } } func benchChunkCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = ctorInts(benchInts).Chunk(benchChunkSize) + _ = ctorInts(benchInts).Chunk(benchChunkSize) - } } } func benchChunkLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Chunk(benchInts, benchChunkSize) + _ = lo.Chunk(benchInts, benchChunkSize) - } } } func benchTakeCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = ctorInts(benchInts).Take(benchTakeN) + _ = ctorInts(benchInts).Take(benchTakeN) - } } } func benchTakeLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Subset(benchInts, 0, uint(benchTakeN)) + _ = lo.Subset(benchInts, 0, uint(benchTakeN)) - } } } func benchContainsCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.Contains(ctorInts(benchInts), benchSize-1) + _ = collection.Contains(ctorInts(benchInts), benchSize-1) - } } } func benchContainsLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.ContainsBy(benchInts, func(v int) bool { return v == benchSize-1 }) + _ = lo.ContainsBy(benchInts, func(v int) bool { return v == benchSize-1 }) - } } } func benchFindCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _ = ctorInts(benchInts).FirstWhere(func(v int) bool { return v == benchSize-1 }) + _, _ = ctorInts(benchInts).FirstWhere(func(v int) bool { return v == benchSize-1 }) - } } } func benchFindLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _ = lo.Find(benchInts, func(v int) bool { return v == benchSize-1 }) + _, _ = lo.Find(benchInts, func(v int) bool { return v == benchSize-1 }) - } } } func benchGroupByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.GroupBySlice(ctorInts(benchInts), func(v int) int { return v % benchGroupByMod }) + _ = collection.GroupBySlice(ctorInts(benchInts), func(v int) int { return v % benchGroupByMod }) - } } } func benchGroupByLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.GroupBy(benchInts, func(v int) int { return v % benchGroupByMod }) + _ = lo.GroupBy(benchInts, func(v int) int { return v % benchGroupByMod }) - } } } func benchCountByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.CountBy(ctorInts(benchIntsDup), func(v int) int { return v }) + _ = collection.CountBy(ctorInts(benchIntsDup), func(v int) int { return v }) - } } } func benchCountByLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.CountValuesBy(benchIntsDup, func(v int) int { return v }) + _ = lo.CountValuesBy(benchIntsDup, func(v int) int { return v }) - } } } func benchCountByValueCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.CountByValue(ctorInts(benchIntsDup)) + _ = collection.CountByValue(ctorInts(benchIntsDup)) - } } } func benchCountByValueLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.CountValues(benchIntsDup) + _ = lo.CountValues(benchIntsDup) - } } } func benchSkipCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = ctorInts(benchInts).Skip(benchSkipN) + _ = ctorInts(benchInts).Skip(benchSkipN) - } } } func benchSkipLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Drop(benchInts, benchSkipN) + _ = lo.Drop(benchInts, benchSkipN) - } } } func benchSkipLastCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = ctorInts(benchInts).SkipLast(benchSkipN) + _ = ctorInts(benchInts).SkipLast(benchSkipN) - } } } func benchSkipLastLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.DropRight(benchInts, benchSkipN) + _ = lo.DropRight(benchInts, benchSkipN) - } } } func benchReverseCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - input := collectionInputForMutating(benchInts) - _ = ctorInts(input).Reverse() + input := collectionInputForMutating(benchInts) + _ = ctorInts(input).Reverse() - } } } func benchReverseLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - copy(workB, benchInts) - _ = lo.Reverse(workB) + copy(workB, benchInts) + _ = lo.Reverse(workB) - } } } func benchShuffleCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - input := collectionInputForMutating(benchInts) - _ = ctorInts(input).Shuffle() + input := collectionInputForMutating(benchInts) + _ = ctorInts(input).Shuffle() - } } } func benchShuffleLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - copy(workB, benchInts) - _ = lo.Shuffle(workB) + copy(workB, benchInts) + _ = lo.Shuffle(workB) - } } } func benchZipCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.Zip(ctorInts(benchInts), ctorInts(benchIntsDup)) + _ = collection.Zip(ctorInts(benchInts), ctorInts(benchIntsDup)) - } } } func benchZipLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Zip2(benchInts, benchIntsDup) + _ = lo.Zip2(benchInts, benchIntsDup) - } } } func benchZipWithCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.ZipWith(ctorInts(benchInts), ctorInts(benchIntsDup), func(a, b int) int { - return a + b - }) + _ = collection.ZipWith(ctorInts(benchInts), ctorInts(benchIntsDup), func(a, b int) int { + return a + b + }) - } } } func benchZipWithLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.ZipBy2(benchInts, benchIntsDup, func(a, b int) int { - return a + b - }) + _ = lo.ZipBy2(benchInts, benchIntsDup, func(a, b int) int { + return a + b + }) - } } } func benchUniqueCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.UniqueComparable(ctorInts(benchIntsDup)) + _ = collection.UniqueComparable(ctorInts(benchIntsDup)) - } } } func benchUniqueLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Uniq(benchIntsDup) + _ = lo.Uniq(benchIntsDup) - } } } func benchUniqueByCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.UniqueBy(ctorInts(benchIntsDup), func(v int) int { return v }) + _ = collection.UniqueBy(ctorInts(benchIntsDup), func(v int) int { return v }) - } } } func benchUniqueByLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.UniqBy(benchIntsDup, func(v int) int { return v }) + _ = lo.UniqBy(benchIntsDup, func(v int) int { return v }) - } } } func benchUnionCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.Union(ctorInts(unionLeft), ctorInts(unionRight)) + _ = collection.Union(ctorInts(unionLeft), ctorInts(unionRight)) - } } } func benchUnionLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Union(unionLeft, unionRight) + _ = lo.Union(unionLeft, unionRight) - } } } func benchIntersectCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.Intersect(ctorInts(intersectLeft), ctorInts(intersectRight)) + _ = collection.Intersect(ctorInts(intersectLeft), ctorInts(intersectRight)) - } } } func benchIntersectLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Intersect(intersectLeft, intersectRight) + _ = lo.Intersect(intersectLeft, intersectRight) - } } } func benchDifferenceCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.Difference(ctorInts(differenceLeft), ctorInts(differenceRight)) + _ = collection.Difference(ctorInts(differenceLeft), ctorInts(differenceRight)) - } } } func benchDifferenceLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _ = lo.Difference(differenceLeft, differenceRight) + _, _ = lo.Difference(differenceLeft, differenceRight) - } } } func benchToMapCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = collection.ToMap(ctorInts(benchInts), func(v int) int { return v }, func(v int) int { return v }) + _ = collection.ToMap(ctorInts(benchInts), func(v int) int { return v }, func(v int) int { return v }) - } } } func benchToMapLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.SliceToMap(benchInts, func(v int) (int, int) { return v, v }) + _ = lo.SliceToMap(benchInts, func(v int) (int, int) { return v, v }) - } } } func benchSumCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = ctorNumericInt(benchInts).Sum() + _ = ctorNumericInt(benchInts).Sum() - } } } func benchSumLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Sum(benchInts) + _ = lo.Sum(benchInts) - } } } func benchMinCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _ = ctorNumericInt(benchInts).Min() + _, _ = ctorNumericInt(benchInts).Min() - } } } func benchMinLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Min(benchInts) + _ = lo.Min(benchInts) - } } } func benchMaxCollection(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _, _ = ctorNumericInt(benchInts).Max() + _, _ = ctorNumericInt(benchInts).Max() - } } } func benchMaxLo(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - for j := 0; j < benchInner; j++ { - _ = lo.Max(benchInts) + _ = lo.Max(benchInts) - } } } From d8bc819577e3285ff4ab195fb79abb2b042ea2d9 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 2 Jan 2026 13:52:56 -0600 Subject: [PATCH 30/30] docs: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7d5a62..b36a4c6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- Fluent, Laravel-style Collections for Go - with generics, chainable pipelines, and expressive data transforms. + Fluent collections for Go - with generics, chainable pipelines, and expressive data transforms. Inspired by Laravel, designed to feel natural in Go.