This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
# 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# Build Docker image
make build
# Install dependencies
go mod download# Scan Docker image for vulnerabilities
make scan
# Scan config files
make scan-config# Serve docs locally at http://localhost:1313
make docs
# Build static docs
make build-docsThe 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
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, EachConcurrency 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
- 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
- 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)
- ✅ 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
- Pipe constraint (
pipe.go:7) -Pipe[T cmp.Ordered]prevents usage with custom types - Last panics (
last.go:5-8) - No empty slice handling
Popular FP utilities not yet implemented: TakeWhile, DropWhile, Scan, First/FirstN, Init, Intersperse, Sliding, FoldRight, Tap, Transpose, Unzip, ParallelReduce, Replicate
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)
Flatmap: Accumulation overhead from repeated appendsGroupBy: Map initialized with capacity 0 (useless hint)
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
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