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>
6.1 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
underscore is a Go library providing functional programming helpers inspired by underscore.js, built on Go 1.18+ generics. The library is organized as a flat structure with individual files for each function, plus a maps subpackage for map-specific utilities.
Development Commands
Testing
# Run all tests (local)
go test ./...
# Run all tests with coverage (local)
go test ./... -coverpkg=./... -coverprofile cov.out -covermode=count
go tool cover -func cov.out
rm cov.out
# Run tests in Docker (preferred for CI/validation)
make test
# Run a single test
go test -run TestFunctionName
# Run tests for a specific file
go test -run TestMap
Building
# Build Docker image
make build
# Install dependencies
go mod download
Linting & Security
# Scan Docker image for vulnerabilities
make scan
# Scan config files
make scan-config
Documentation
# Serve docs locally at http://localhost:1313
make docs
# Build static docs
make build-docs
Architecture
Code Organization
The library uses a flat structure where each function is implemented in its own file:
<function>.go- implementation<function>_test.go- tests
Example: filter.go + filter_test.go, map.go + map_test.go
Core Patterns
Generic Functions: Most functions use Go generics with constraints from cmp.Ordered or custom type parameters. Functions operate on slices and return new slices (immutable style).
Pipe Chain: The Pipe[T] struct enables method chaining for ordered types. Methods that return slices continue the chain, while methods that return values (like All, Any, Reduce) break the chain and return the final value.
// pipe.go defines Pipe[T cmp.Ordered]
// Chain-continuing: Filter, Map
// Chain-breaking: All, Any, Reduce, Min, Max, Partition, Find, Each
Concurrency Helpers: ParallelMap and ParallelFilter use worker pools with:
- Context-based cancellation
- Order preservation (results match input order)
- First-error-wins semantics
- Default workers = GOMAXPROCS if workers <= 0
Implementation detail: Uses sync.Once to capture first error and cancel context immediately.
Subpackages:
maps/- Map-specific utilities (Keys,Values,Map)- Uses type alias
M[K, V] = map[K]Vfor cleaner signatures Mapfunction allows transforming map entries
- Uses type alias
Testing Conventions
- Use
testify/assertfor assertions - Test file names match source files with
_test.gosuffix - Table-driven tests are common (see
map_test.go,filter_test.go) - Internal tests (using
package underscorerather thanpackage underscore_test) are used sparingly for testing unexported functions
Key Constraints
- Minimum Go version: 1.24.2 (see go.mod)
- Generic constraints: Most collection functions require
cmp.Orderedtypes; some usecomparableor no constraints - Order preservation:
ParallelMapandParallelFilterguarantee output order matches input order - No mutation: Functions return new slices;
UniqueInPlaceis the exception (in-place deduplication)
Known Limitations
Recently Fixed (2025-11-14)
- ✅ Filter allocation - Now pre-allocates with
make([]T, 0, len(values))(90% fewer allocations) - ✅ OrderBy algorithm - Replaced bubble sort with
slices.SortFunc(629x faster for large datasets) - ✅ Partition allocation - Now pre-allocates both result slices
- ✅ Max/Min empty slices - Now panics with clear message: "underscore.Max: empty slice"
- ✅ Drop semantics - Fixed to drop first N elements (breaking change). Old behavior available as
RemoveAt
API Design Issues
- Pipe constraint (
pipe.go:7) -Pipe[T cmp.Ordered]prevents usage with custom types - Last panics (
last.go:5-8) - No empty slice handling
Missing Features
Popular FP utilities not yet implemented: TakeWhile, DropWhile, Scan, First/FirstN, Init, Intersperse, Sliding, FoldRight, Tap, Transpose, Unzip, ParallelReduce, Replicate
Performance Characteristics
Good Performance Patterns
Filterpre-allocates:make([]T, 0, len(values))✅ (Fixed 2025-11-14)Mappre-allocates:make([]P, 0, len(values))Partitionpre-allocates:make([]T, 0, len(values))for both slices ✅ (Fixed 2025-11-14)Chunkpre-calculates capacity:(l+n-1)/nParallelFilterpre-counts before allocationOrderByusesslices.SortFunc: O(n log n) performance ✅ (Fixed 2025-11-14)
Remaining Performance Issues
Flatmap: Accumulation overhead from repeated appendsGroupBy: Map initialized with capacity 0 (useless hint)
When to Use ParallelMap vs Map
Use ParallelMap when:
- Processing 100+ elements with expensive operations (>1ms per element)
- Operations are CPU-bound (not I/O-bound with shared resources)
- Order preservation is required
- Context cancellation is needed
Use regular Map when:
- Small slices (<100 elements)
- Fast operations (<100µs per element)
- Avoiding goroutine overhead matters
- Simple transformations without error handling
Worker count guidelines:
- Default (workers=0): Uses
runtime.GOMAXPROCS(0)- good starting point - CPU-bound: Use GOMAXPROCS or GOMAXPROCS*2
- I/O-bound: Can use higher values (10-100) if not sharing resources
Contributing Notes
When adding new functions:
- Create both
<function>.goand<function>_test.go - Add examples in comments using Go doc format
- Pre-allocate slices with
make([]T, 0, len(input))when output size is similar to input - Document panic conditions (empty slices, nil inputs, invalid indices)
- Add edge case tests (empty, single element, nil)
- If the function applies to Pipe chains, add a method to
pipe.go - Update README.md function list if adding new collection functions
- Follow SemVer for version numbers
- Ensure all tests pass:
make test
When fixing bugs:
- Add regression tests before fixing
- Run benchmarks if performance-related:
go test -bench=. -benchmem - Check for similar issues in other functions