BREAKING CHANGE: Drop function now correctly drops first N elements
instead of removing element at specific index.
Changes:
- Renamed old Drop behavior to RemoveAt function
- Implemented correct Drop semantics (drop first N elements)
- Added comprehensive tests for both functions
Drop (NEW behavior):
- Drop([]int{1,2,3,4,5}, 2) → [3,4,5] (drops first 2 elements)
- Returns empty slice if n >= len(values)
- Returns original slice if n <= 0
RemoveAt (OLD Drop behavior):
- RemoveAt([]int{1,2,3,4,5}, 2) → [1,2,4,5] (removes index 2)
- Returns original slice if index out of bounds
- Pre-allocates with capacity len(values)-1
Tests added:
- Drop: 5 tests (basic, none, all, empty, single)
- RemoveAt: 6 tests (basic, first, last, bounds, empty, single)
Documentation updated:
- README.md: Added RemoveAt to function list
- CLAUDE.md: Marked Drop semantics as fixed
- ACTION_PLAN.md: Updated completion status
Migration guide:
- Old: Drop(slice, index) → New: RemoveAt(slice, index)
- New Drop usage: Drop(slice, n) drops first n elements
Coverage: 98.8% (maintained)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
27 KiB
Action Plan: Underscore Library Improvements
Status: In Progress - Week 1 (4/5 completed) Overall Quality Score: 8.2/10 → 9.0/10 (estimated after fixes) Generated: 2025-11-14 Last Updated: 2025-11-14
This document outlines prioritized improvements for the underscore Go library based on comprehensive codebase review.
Completion Status
✅ Completed Issues
- Issue 1: Filter pre-allocation (90% fewer allocations) - Commit
92b6463 - Issue 2: OrderBy bubble sort replacement (629x faster) - Commit
7caa23e - Issue 3: Partition pre-allocation - Commit
46d52e3 - Issue 4: Max/Min empty slice handling - Commit
a194355 - Issue 5: Add edge case tests - Commit
106b713 - Issue 6: Drop semantics clarification (breaking change) - Ready to commit
🔄 In Progress
- None currently
⏳ Pending
- See sections below for remaining issues
Priority Matrix
🔴 Critical (Week 1) - 2-3 hours total
High impact, low effort fixes that significantly improve performance and stability.
🟡 High Priority (Week 2) - 5-6 hours total
Important improvements for API consistency and testing coverage.
🟢 Medium Priority (Week 3) - 4-5 hours total
Additional optimizations and quality improvements.
🔵 Future Enhancements
New features to add based on user demand.
🔴 CRITICAL ISSUES (Week 1)
1. Fix Filter Pre-allocation ⏱️ 2 min ✅ COMPLETED
File: filter.go:4
Issue: No pre-allocation causes O(n) allocations instead of O(1)
Impact: 2-5x performance improvement
Status: ✅ Fixed in commit 92b6463
Results: 90% fewer allocations (10→1), 8% faster execution
Current Code
func Filter[T any](values []T, predicate func(T) bool) (res []T) {
for _, v := range values {
if predicate(v) {
res = append(res, v) // ❌ No pre-allocation
}
}
return res
}
Fixed Code
func Filter[T any](values []T, predicate func(T) bool) (res []T) {
res = make([]T, 0, len(values)) // ✅ Pre-allocate
for _, v := range values {
if predicate(v) {
res = append(res, v)
}
}
return res
}
Steps
- Add benchmark test first to measure improvement
- Change line 4: Add
res = make([]T, 0, len(values)) - Run tests:
go test ./... -v - Run benchmark:
go test -bench=BenchmarkFilter -benchmem - Commit: "perf: pre-allocate Filter result slice"
2. Replace OrderBy Bubble Sort ⏱️ 5 min ✅ COMPLETED
File: orderBy.go:7-27
Issue: O(n²) bubble sort with TODO comment
Impact: O(n²) → O(n log n) complexity
Status: ✅ Fixed in commit 7caa23e
Results: 629x faster for large datasets (1000 items), resolved TODO
Current Code
func OrderBy[T any](list []T, predicate func(T, T) bool) []T {
swaps := true
var tmp T
//todo: replace with a faster algorithm, this one is pretty simple
for swaps {
swaps = false
for i := 0; i < len(list)-1; i++ {
if predicate(list[i], list[i+1]) {
swaps = true
tmp = list[i]
list[i] = list[i+1]
list[i+1] = tmp
}
}
}
return list
}
Fixed Code
import "slices"
// OrderBy orders a slice by a field value within a struct, the predicate allows you
// to pick the fields you want to orderBy. Use > for ASC or < for DESC
// Uses O(n log n) sorting algorithm. Mutates the input slice.
//
// func (left Person, right Person) bool { return left.Age > right.Age }
func OrderBy[T any](list []T, predicate func(T, T) bool) []T {
slices.SortFunc(list, func(a, b T) int {
if predicate(a, b) {
return 1
}
if predicate(b, a) {
return -1
}
return 0
})
return list
}
Steps
- Add benchmark test to measure improvement
- Replace entire function body with
slices.SortFunc - Update doc comment to mention O(n log n) and mutation
- Run tests:
go test ./... -v - Run benchmark:
go test -bench=BenchmarkOrderBy -benchmem - Commit: "perf: replace bubble sort with slices.SortFunc in OrderBy"
3. Fix Partition Pre-allocation ⏱️ 2 min ✅ COMPLETED
File: partition.go:6-7
Issue: No capacity hints cause repeated allocations
Impact: Fewer allocations during split
Status: ✅ Fixed in commit 46d52e3
Results: Reduced allocations from O(log n) to O(1) per slice
Current Code
func Partition[T any](values []T, predicate func(T) bool) ([]T, []T) {
keep := make([]T, 0) // ❌ No capacity hint
reject := make([]T, 0) // ❌ No capacity hint
for _, v := range values {
if predicate(v) {
keep = append(keep, v)
} else {
reject = append(reject, v)
}
}
return keep, reject
}
Fixed Code
func Partition[T any](values []T, predicate func(T) bool) ([]T, []T) {
keep := make([]T, 0, len(values)) // ✅ Pre-allocate
reject := make([]T, 0, len(values)) // ✅ Pre-allocate
for _, v := range values {
if predicate(v) {
keep = append(keep, v)
} else {
reject = append(reject, v)
}
}
return keep, reject
}
Steps
- Change lines 6-7 to add capacity hint
- Run tests:
go test ./... -v - Commit: "perf: pre-allocate Partition result slices"
4. Handle Max/Min Empty Slices ⏱️ 30 min ✅ COMPLETED
Files: max.go:8-16, min.go:8-16
Issue: Panics on empty slices
Impact: Prevent runtime panics
Status: ✅ Fixed in commit a194355 (Option B: Non-breaking)
Results: Clear panic messages, documented behavior, added tests
Current Code
func Max[T cmp.Ordered](values []T) T {
max := values[0] // ❌ Panic on empty slice
for _, v := range values {
if v > max {
max = v
}
}
return max
}
Option A: Return Error (Recommended)
import "errors"
// Max returns the maximum value in the slice.
// Returns error if the slice is empty.
// This function can currently only compare numbers reliably.
// This function uses operator <.
func Max[T cmp.Ordered](values []T) (T, error) {
var zero T
if len(values) == 0 {
return zero, errors.New("cannot find max of empty slice")
}
max := values[0]
for _, v := range values {
if v > max {
max = v
}
}
return max, nil
}
Option B: Document Panic (Faster, breaking change avoided)
// Max returns the maximum value in the slice.
// Panics if values is empty.
// This function can currently only compare numbers reliably.
// This function uses operator <.
func Max[T cmp.Ordered](values []T) T {
if len(values) == 0 {
panic("underscore.Max: empty slice")
}
max := values[0]
for _, v := range values {
if v > max {
max = v
}
}
return max
}
Steps (Choose one approach)
For Option A (Breaking Change):
- Update
max.goandmin.goto return(T, error) - Update
pipe.goMax/Min methods to return error - Update all test files to check error return
- Update README.md examples if needed
- Run tests:
go test ./... -v - Document breaking change in CHANGELOG
- Commit: "fix!: Max/Min return error on empty slices"
For Option B (Non-Breaking):
- Add length check with explicit panic message
- Update doc comments to document panic behavior
- Add tests for panic behavior:
assert.Panics(t, func() { Max([]int{}) }) - Run tests:
go test ./... -v - Commit: "fix: add explicit panic with message for Max/Min on empty slices"
5. Add Edge Case Tests ⏱️ 1 hour
Files: Create/update *_test.go files
Issue: Missing tests for empty slices, nil inputs, single elements
Impact: Catch regressions and edge cases
Test Cases to Add
Empty Slice Tests (filter_test.go, max_test.go, min_test.go, etc.)
func TestFilterEmpty(t *testing.T) {
result := Filter([]int{}, func(n int) bool { return n > 0 })
assert.Empty(t, result)
}
func TestMaxEmpty(t *testing.T) {
// If using Option A from above
_, err := Max([]int{})
assert.Error(t, err)
// If using Option B from above
assert.Panics(t, func() { Max([]int{}) })
}
func TestLastEmpty(t *testing.T) {
assert.Panics(t, func() { Last([]int{}) })
}
Single Element Tests
func TestFilterSingleElement(t *testing.T) {
result := Filter([]int{5}, func(n int) bool { return n > 0 })
assert.Equal(t, []int{5}, result)
}
func TestPartitionSingleElement(t *testing.T) {
keep, reject := Partition([]int{5}, func(n int) bool { return n > 3 })
assert.Equal(t, []int{5}, keep)
assert.Empty(t, reject)
}
Large Slice Tests
func TestFilterLargeSlice(t *testing.T) {
large := make([]int, 10000)
for i := range large {
large[i] = i
}
result := Filter(large, func(n int) bool { return n%2 == 0 })
assert.Equal(t, 5000, len(result))
}
Nil Predicate Tests (if applicable)
func TestFilterNilPredicate(t *testing.T) {
assert.Panics(t, func() {
Filter([]int{1, 2, 3}, nil)
})
}
Steps
- Create test plan spreadsheet/checklist of all functions
- Add edge case tests for each function
- Run tests:
go test ./... -v -cover - Fix any failures discovered
- Commit: "test: add comprehensive edge case tests"
🟡 HIGH PRIORITY (Week 2)
6. Clarify Drop Semantics ⏱️ 30 min
File: drop.go
Issue: Function name suggests "drop first N" but removes element at index
Impact: API clarity and user expectations
Current Behavior
Drop([]int{1,2,3,4,5}, 2) // Returns [1,2,4,5] - removes index 2
Expected Behavior (based on underscore.js/Haskell)
Drop([]int{1,2,3,4,5}, 2) // Should return [3,4,5] - drop first 2
Solution: Add New Functions
Keep current Drop but rename to RemoveAt
// RemoveAt returns a new slice with the element at the given index removed.
// Returns original slice if index is out of bounds.
func RemoveAt[T any](values []T, index int) []T {
if index < 0 || index >= len(values) {
return values
}
res := make([]T, 0, len(values)-1)
for i, value := range values {
if i != index {
res = append(res, value)
}
}
return res
}
Add new Drop with correct semantics
// Drop returns a new slice with the first n elements removed.
// If n is greater than the slice length, returns an empty slice.
// If n is negative, returns the original slice.
func Drop[T any](values []T, n int) []T {
if n <= 0 {
return values
}
if n >= len(values) {
return []T{}
}
res := make([]T, len(values)-n)
copy(res, values[n:])
return res
}
Add DropWhile
// DropWhile drops elements from the beginning while predicate is true.
// Returns remaining elements once predicate returns false.
func DropWhile[T any](values []T, predicate func(T) bool) []T {
for i, v := range values {
if !predicate(v) {
res := make([]T, len(values)-i)
copy(res, values[i:])
return res
}
}
return []T{}
}
Steps
- Create
remove_at.gowith new function - Create
remove_at_test.gowith tests - Update
drop.gowith new semantics - Update
drop_test.gowith new tests - Add
drop_while.goand tests - Update README.md function list
- Document breaking change in CHANGELOG
- Commit: "feat!: fix Drop semantics and add RemoveAt, DropWhile"
7. Add Benchmarks ⏱️ 2 hours
Files: Create benchmark_test.go or add to existing test files
Issue: No performance baselines exist
Impact: Track performance regressions
Benchmarks to Add
Core Functions
func BenchmarkFilter(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Filter(data, func(n int) bool { return n%2 == 0 })
}
}
func BenchmarkMap(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Map(data, func(n int) int { return n * 2 })
}
}
func BenchmarkReduce(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Reduce(data, func(n, acc int) int { return n + acc }, 0)
}
}
OrderBy Comparison
func BenchmarkOrderBy(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = 1000 - i // Reverse order
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
dataCopy := make([]int, len(data))
copy(dataCopy, data)
OrderBy(dataCopy, func(a, b int) bool { return a > b })
}
}
Concurrency Benchmarks
func BenchmarkParallelMap(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
ctx := context.Background()
for _, workers := range []int{1, 2, 4, 8, 16} {
b.Run(fmt.Sprintf("workers=%d", workers), func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
ParallelMap(ctx, data, workers, func(ctx context.Context, n int) (int, error) {
return n * 2, nil
})
}
})
}
}
func BenchmarkMapVsParallelMap(b *testing.B) {
data := make([]int, 10000)
for i := range data {
data[i] = i
}
ctx := context.Background()
b.Run("Map", func(b *testing.B) {
for i := 0; i < b.N; i++ {
Map(data, func(n int) int { return n * 2 })
}
})
b.Run("ParallelMap", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ParallelMap(ctx, data, 0, func(ctx context.Context, n int) (int, error) {
return n * 2, nil
})
}
})
}
Memory Allocation Benchmarks
func BenchmarkPartition(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Partition(data, func(n int) bool { return n%2 == 0 })
}
}
func BenchmarkUnique(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i % 100 // Many duplicates
}
b.Run("Unique", func(b *testing.B) {
for i := 0; i < b.N; i++ {
Unique(data)
}
})
b.Run("UniqueInPlace", func(b *testing.B) {
for i := 0; i < b.N; i++ {
dataCopy := make([]int, len(data))
copy(dataCopy, data)
UniqueInPlace(dataCopy)
}
})
}
Steps
- Create benchmarks for core functions
- Run baseline:
go test -bench=. -benchmem > bench_before.txt - Document baseline results
- Add benchmark CI job (optional)
- Commit: "test: add comprehensive benchmarks for core functions"
8. Improve Documentation ⏱️ 1 hour
Files: Various .go files, CLAUDE.md, README.md
Issue: Missing edge case warnings, performance notes
Impact: Better developer experience
Documentation Updates Needed
Add panic warnings
// Max returns the maximum value in the slice.
// Panics if values is empty. // ← Add this
// This function can currently only compare numbers reliably.
// This function uses operator <.
func Max[T cmp.Ordered](values []T) T
// Last returns the last element from the slice.
// Panics if the slice is empty. // ← Add this
func Last[T any](values []T) T
Add complexity notes
// OrderBy orders a slice by a field value.
// Uses O(n log n) sorting. Mutates the input slice. // ← Add this
// The predicate allows you to pick the fields you want to orderBy.
// Use > for ASC or < for DESC
Add constraint explanations
// Pipe enables method chaining for ordered types.
// Type parameter T must be cmp.Ordered because Max/Min methods require it. // ← Add this
type Pipe[T cmp.Ordered] struct {
Value []T
}
Update README.md
- Add performance section
- Add "When to use" guidelines for ParallelMap
- Add edge case handling notes
Steps
- Review all function doc comments
- Add panic conditions where applicable
- Add complexity notes for non-O(n) operations
- Update README.md with performance section
- Update docs/ Hugo site if needed
- Commit: "docs: add panic warnings and performance notes"
🟢 MEDIUM PRIORITY (Week 3)
9. Fix Flatmap Allocation ⏱️ 30 min
File: flatmap.go:6
Issue: No pre-allocation causes repeated allocations
Impact: ~30-50% performance improvement
Current Code
func Flatmap[T any](values []T, mapper func(n T) []T) []T {
res := make([]T, 0) // ❌ No pre-allocation
for _, v := range values {
vs := mapper(v)
res = append(res, vs...)
}
return res
}
Option A: Estimate Average Size
func Flatmap[T any](values []T, mapper func(n T) []T) []T {
// Estimate capacity assuming avg 2-3 items per map
res := make([]T, 0, len(values)*2)
for _, v := range values {
vs := mapper(v)
res = append(res, vs...)
}
return res
}
Option B: Two-Pass (More Memory Efficient)
func Flatmap[T any](values []T, mapper func(n T) []T) []T {
// First pass: calculate total size
totalSize := 0
mapped := make([][]T, len(values))
for i, v := range values {
mapped[i] = mapper(v)
totalSize += len(mapped[i])
}
// Second pass: allocate exact size and copy
res := make([]T, 0, totalSize)
for _, vs := range mapped {
res = append(res, vs...)
}
return res
}
Steps
- Add benchmark test
- Choose approach based on typical use cases
- Update implementation
- Run tests and benchmarks
- Commit: "perf: improve Flatmap allocation strategy"
10. Fix GroupBy Map Initialization ⏱️ 2 min
File: groupby.go:5
Issue: Capacity hint of 0 is useless for maps
Impact: Minor allocation improvement
Current Code
func GroupBy[K comparable, V any](values []V, f func(V) K) map[K][]V {
res := make(map[K][]V, 0) // ❌ Capacity 0 is useless
...
}
Fixed Code
func GroupBy[K comparable, V any](values []V, f func(V) K) map[K][]V {
res := make(map[K][]V, len(values)/10) // ✅ Estimate
...
}
Steps
- Update capacity hint (len/10 or just len)
- Run tests
- Commit: "perf: improve GroupBy map initialization"
11. Relax Pipe Constraint ⏱️ 2 hours
File: pipe.go:7 and method signatures
Issue: Pipe[T cmp.Ordered] prevents usage with custom types
Impact: Broader API usability
This is a breaking change that requires careful consideration.
Current Limitation
type Pipe[T cmp.Ordered] struct {
Value []T
}
// Cannot use with:
type Person struct { Name string; Age int }
NewPipe([]Person{...}) // ❌ Error: Person does not satisfy cmp.Ordered
Option A: Make Pipe Generic, Constrain Methods
// Pipe can now work with any type
type Pipe[T any] struct {
Value []T
}
// Methods that need ordering constrain themselves
func (c Pipe[T]) Max() T where T: cmp.Ordered { // ❌ Go doesn't support this
return Max(c.Value)
}
Problem: Go doesn't support method-level constraints different from type-level.
Option B: Create Two Pipe Types
// Generic pipe for any type
type Pipe[T any] struct {
Value []T
}
// Ordered pipe with additional methods
type OrderedPipe[T cmp.Ordered] struct {
Pipe[T] // Embed generic pipe
}
// Max/Min only on OrderedPipe
func (c OrderedPipe[T]) Max() T {
return Max(c.Value)
}
// Factory functions
func NewPipe[T any](value []T) Pipe[T] {
return Pipe[T]{Value: value}
}
func NewOrderedPipe[T cmp.Ordered](value []T) OrderedPipe[T] {
return OrderedPipe[T]{Pipe: Pipe[T]{Value: value}}
}
Option C: Remove Max/Min from Pipe
// Simplest solution: just remove problematic methods
type Pipe[T any] struct {
Value []T
}
// Users can break chain for Max/Min
result := NewPipe(values).
Filter(...).
Map(...).
Value
max := Max(result) // Outside pipe chain
Steps
- Decide on approach (discuss with maintainers)
- Implement chosen solution
- Update all tests
- Update documentation
- Add migration guide
- Document breaking change
- Commit: "feat!: relax Pipe type constraint"
12. Add Stress Tests ⏱️ 1 hour
Files: Create stress_test.go
Issue: No tests with large data or high concurrency
Impact: Catch race conditions and memory issues
Test Cases
Large Data Tests
func TestFilterLargeData(t *testing.T) {
if testing.Short() {
t.Skip("Skipping stress test in short mode")
}
large := make([]int, 1_000_000)
for i := range large {
large[i] = i
}
result := Filter(large, func(n int) bool { return n%2 == 0 })
assert.Equal(t, 500_000, len(result))
}
Concurrency Stress Tests
func TestParallelMapHighConcurrency(t *testing.T) {
if testing.Short() {
t.Skip("Skipping stress test in short mode")
}
data := make([]int, 10000)
for i := range data {
data[i] = i
}
ctx := context.Background()
// Test with many workers
result, err := ParallelMap(ctx, data, 100, func(ctx context.Context, n int) (int, error) {
time.Sleep(time.Microsecond) // Simulate work
return n * 2, nil
})
assert.NoError(t, err)
assert.Equal(t, len(data), len(result))
}
func TestParallelMapCancellation(t *testing.T) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
_, err := ParallelMap(ctx, data, 4, func(ctx context.Context, n int) (int, error) {
time.Sleep(100 * time.Millisecond) // Slow work
return n, nil
})
assert.Error(t, err)
}
Race Condition Tests
func TestParallelMapNoRaces(t *testing.T) {
// Run with: go test -race
data := make([]int, 1000)
for i := range data {
data[i] = i
}
ctx := context.Background()
for i := 0; i < 100; i++ {
_, err := ParallelMap(ctx, data, 8, func(ctx context.Context, n int) (int, error) {
return n * 2, nil
})
assert.NoError(t, err)
}
}
Steps
- Create
stress_test.go - Add stress test flag handling
- Run:
go test -v -run Stress - Run:
go test -race - Commit: "test: add stress tests for large data and concurrency"
13. Document Last Edge Cases ⏱️ 10 min
File: last.go:5-8
Issue: Panics on empty slices, not documented
Impact: Prevent user surprises
Current Code
// Last returns the last element from the slice.
func Last[T any](values []T) T {
n := len(values)
return values[n-1] // ❌ Panics on empty
}
Fixed Code
// Last returns the last element from the slice.
// Panics if the slice is empty.
func Last[T any](values []T) T {
if len(values) == 0 {
panic("underscore.Last: empty slice")
}
n := len(values)
return values[n-1]
}
Steps
- Add length check with explicit panic
- Update doc comment
- Add test:
assert.Panics(t, func() { Last([]int{}) }) - Commit: "fix: add explicit panic for Last on empty slice"
🔵 FUTURE ENHANCEMENTS
Missing Functional Programming Utilities
Add these based on user demand and usage patterns:
14. TakeWhile / DropWhile ⏱️ 1 hour
func TakeWhile[T any](values []T, predicate func(T) bool) []T
func DropWhile[T any](values []T, predicate func(T) bool) []T
15. Scan (Reduce with history) ⏱️ 30 min
func Scan[T, P any](values []T, acc P, fn func(T, P) P) []P
// Example: Scan([]int{1,2,3,4}, 0, +) → [1, 3, 6, 10]
16. First / FirstN ⏱️ 20 min
func First[T any](values []T) (T, error)
func FirstN[T any](values []T, n int) []T
17. Init (all but last) ⏱️ 15 min
func Init[T any](values []T) ([]T, T)
18. Intersperse ⏱️ 20 min
func Intersperse[T any](values []T, separator T) []T
19. Sliding Window ⏱️ 30 min
func Sliding[T any](values []T, size int) [][]T
20. FoldRight ⏱️ 15 min
func FoldRight[T, P any](values []T, acc P, fn func(T, P) P) P
21. Tap (for debugging) ⏱️ 15 min
func Tap[T any](values []T, fn func(T)) []T
22. Transpose ⏱️ 30 min
func Transpose[T any](matrix [][]T) [][]T
23. Unzip ⏱️ 20 min
func Unzip[L, R any](pairs []Tuple[L, R]) ([]L, []R)
24. ParallelReduce ⏱️ 2 hours
func ParallelReduce[T, P any](ctx context.Context, values []T, workers int,
fn func(context.Context, T, P) (P, error), acc P) (P, error)
25. Replicate ⏱️ 10 min
func Replicate[T any](count int, value T) []T
Testing Strategy
Before Any Changes
- Run full test suite:
go test ./... -v -cover - Document current coverage:
go test -coverprofile=coverage.out && go tool cover -func=coverage.out - Create baseline benchmarks:
go test -bench=. -benchmem > baseline.txt
After Each Change
- Run affected tests:
go test -run TestFunction -v - Run full suite:
go test ./... -v - Check coverage: Coverage should not decrease
- Run benchmarks:
go test -bench=BenchmarkFunction -benchmem - Run race detector:
go test -race
CI Integration
Add GitHub Actions workflow:
- name: Test
run: go test ./... -v -race -coverprofile=coverage.out
- name: Benchmark
run: go test -bench=. -benchmem
Breaking Changes Policy
When making breaking changes:
-
Document in CHANGELOG.md
- What changed
- Why it changed
- Migration path
-
Update Version
- Major version bump (v0.7.0 → v0.8.0)
- Follow SemVer strictly
-
Provide Deprecation Period (if possible)
- Keep old function with
Deprecated:doc comment - Add new function alongside
- Remove in next major version
- Keep old function with
-
Add Migration Guide
- Before/after code examples
- Search/replace patterns
- Common pitfalls
Success Metrics
After completing all critical and high priority items:
- ✅ Test coverage remains >99%
- ✅ Filter performance improves 2-5x
- ✅ OrderBy performance improves 10-100x for large lists
- ✅ Zero panics on empty slices (or documented)
- ✅ Benchmark suite covering all core functions
- ✅ API inconsistencies resolved
- ✅ All edge cases tested
Target Quality Score: 9.5/10
Notes
- All time estimates are approximate
- Test thoroughly after each change
- Consider user impact for breaking changes
- Gather community feedback before major API changes
- Update documentation as you go
- Run benchmarks to verify improvements
Generated by comprehensive codebase review on 2025-11-14.