https://github.com/binaryphile/fluentfp
simple, readable fluent slices and options in Go
https://github.com/binaryphile/fluentfp
Last synced: 4 months ago
JSON representation
simple, readable fluent slices and options in Go
- Host: GitHub
- URL: https://github.com/binaryphile/fluentfp
- Owner: binaryphile
- License: mit
- Created: 2024-07-11T23:37:32.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2026-02-12T07:36:23.000Z (4 months ago)
- Last Synced: 2026-02-12T16:49:48.031Z (4 months ago)
- Language: Go
- Homepage:
- Size: 691 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# fluentfp
**Fluent** functional programming for Go: fewer bugs, less code, predictable performance.
> **Summary:** Eliminate control structures, eliminate the bugs they enable.
> Mixed codebases see 26% complexity reduction; pure pipelines drop 95%.
> The win isn't lines saved—it's bugs that become unwritable.
fluentfp is a small set of composable utilities for data transformation and type safety in Go.
Fluent operations chain method calls on a single line—no intermediate variables, no loop scaffolding.
See [pkg.go.dev](https://pkg.go.dev/github.com/binaryphile/fluentfp) for complete API documentation.
## Quick Start
```bash
go get github.com/binaryphile/fluentfp
```
```go
// Before: loop mechanics interleaved with intent
var names []string
for _, u := range users {
if u.IsActive() {
names = append(names, u.Name)
}
}
// After: just intent
names := slice.From(users).KeepIf(User.IsActive).ToString(User.GetName)
```
## The Problem
Loop mechanics create bugs regardless of developer skill:
- **Accumulator errors**: forgot to increment, wrong variable
- **Defer in loop**: resources pile up until function returns
- **Index typos**: `i+i` instead of `i+1`
- **Ignored errors**: `_ = fn()` silently continues when "impossible" errors occur
C-style loops add off-by-one errors: `i <= n` instead of `i < n`.
These bugs compile, pass review, and look correct. They continue to appear in highly-reviewed, very public projects. If the construct allows an error, it will eventually happen.
## The Solution
*Correctness by construction*: design code so errors can't occur.
| Bug Class | Why It Happens | fluentfp Elimination |
|-----------|----------------|---------------------|
| Accumulator error | Manual state tracking | `Fold` manages state |
| Defer in loop | Loop body accumulates | No loop body |
| Index typo | Manual index math | Predicates operate on values |
| Off-by-one (C-style) | Manual bounds | Iterate collection, not indices |
| Ignored error | `_ =` discards error | `must.BeNil` enforces invariant |
## Measurable Impact
| Codebase Type | Code Reduction | Complexity Reduction |
|---------------|----------------|---------------------|
| Mixed (typical) | 12% | 26% |
| Pure pipeline | 47% | 95% |
*Complexity measured via `scc` (cyclomatic complexity approximation). See [methodology](methodology.md#code-metrics-tool-scc).*
## Performance
| Operation | Loop | Chain | Result |
|-----------|------|-------|--------|
| Filter only | 5.6 μs | 5.5 μs | **Equal** |
| Filter + Map | 3.1 μs | 7.6 μs | Loop 2.5× faster |
| Count only | 0.26 μs | 7.6 μs | Loop 29× faster |
Single operations equal properly-written loops (both pre-allocate). In practice, many loops use naive append for simplicity—chains beat those. Multi-operation chains allocate per operation. See [full benchmarks](methodology.md#benchmark-results).
## When to Use fluentfp
**High yield** (adopt broadly):
- Data pipelines, ETL, report generators
- Filter/map/fold patterns
- Field extraction from collections
**Medium yield** (adopt selectively):
- API handlers with data transformation
- Config validation
**Low yield** (probably skip):
- I/O-heavy code with minimal transformation
- Graph/tree traversal
- Streaming/channel-based pipelines
## When to Use Loops
- **Channel consumption**: `for r := range ch`
- **Complex control flow**: break, continue, early return
- **Index-dependent logic**: when you need `i` for more than indexing
## Parallelism Readiness
Pure functions + immutable data = safe parallelism.
**Note:** fluentfp does not provide parallel operations. But the patterns it encourages—pure transforms, no shared state—are exactly what makes code *parallel-ready* when you need it.
```go
// With errgroup (idiomatic Go)
import "golang.org/x/sync/errgroup"
var g errgroup.Group
results := make([]Result, len(items))
for i, item := range items {
i, item := i, item // capture by value for closure
g.Go(func() error {
results[i] = transform(item) // Safe: transform is pure, i is unique
return nil
})
}
g.Wait()
```
**Benchmarked crossover (Go, 8 cores):**
| N | Sequential | Parallel | Speedup | Verdict |
|---|------------|----------|---------|---------|
| 100 | 5.6μs | 9.3μs | 0.6× | Sequential wins |
| 1,000 | 56μs | 40μs | 1.4× | Parallel starts winning |
| 10,000 | 559μs | 200μs | 2.8× | Parallel wins |
| 100,000 | 5.6ms | 1.4ms | 4.0× | Parallel wins decisively |
**When to parallelize:**
- N > 1K items AND CPU-bound transform → yes
- N < 500 OR transform < 100ns → no (overhead dominates)
- I/O-bound (HTTP calls, disk) → yes (waiting is free to parallelize)
**Key insight:** The discipline investment—writing pure transforms—pays off when you need parallelism and don't have to refactor first.
*Reproduce these benchmarks: `go test -bench=. -benchmem ./examples/`*
## Packages
| Package | Purpose | Key Functions |
|---------|---------|---------------|
| [slice](slice/) | Collection transforms | `KeepIf`, `RemoveIf`, `Fold`, `ToString` |
| [option](option/) | Nil safety | `Of`, `Get`, `Or`, `IfNotZero`, `IfNotNil` |
| [either](either/) | Sum types | `Left`, `Right`, `Fold`, `Map` |
| [must](must/) | Fallible funcs → HOF args | `Get`, `BeNil`, `Of` |
| [value](value/) | Conditional value selection | `Of().When().Or()` |
| [pair](tuple/pair/) | Zip slices | `Zip`, `ZipWith` |
| [lof](lof/) | Lower-order function wrappers | `Len`, `Println`, `StringLen` |
## Installation
```bash
go get github.com/binaryphile/fluentfp
```
```go
import "github.com/binaryphile/fluentfp/slice"
import "github.com/binaryphile/fluentfp/option"
```
## Package Highlights
### slice
Fluent collection operations with method chaining:
```go
// Filter and extract
actives := slice.From(users).KeepIf(User.IsActive)
names := slice.From(users).ToString(User.GetName)
// Map to arbitrary types
users := slice.MapTo[User](ids).Map(FetchUser)
// Reduce
total := slice.Fold(amounts, 0.0, sumFloat64)
```
### option
Eliminate nil panics with explicit optionality:
```go
// Create
opt := option.Of(user) // always ok
opt := option.IfNotZero(name) // ok if non-zero (comparable types)
opt := option.IfNotNil(ptr) // ok if not nil (pointer types)
// Extract
user, ok := opt.Get() // comma-ok
user := opt.Or(defaultUser) // with fallback
```
### either
Sum types for values that are one of two possible types:
```go
// Create
fail := either.Left[string, int]("fail")
ok42 := either.Right[string, int](42)
// Extract with comma-ok
if fortyTwo, ok := ok42.Get(); ok {
fmt.Println(fortyTwo) // 42
}
// Fold: handle both cases exhaustively
// formatLeft returns an error message.
formatLeft := func(err string) string { return "Error: " + err }
// formatRight returns a success message.
formatRight := func(n int) string { return fmt.Sprintf("Got: %d", n) }
msg := either.Fold(ok42, formatLeft, formatRight) // "Got: 42"
msg = either.Fold(fail, formatLeft, formatRight) // "Error: fail"
```
### must
Make error invariants explicit. Every `_ = fn()` should be `must.BeNil(fn())`:
```go
_ = os.Setenv("KEY", value) // Silent corruption if error
must.BeNil(os.Setenv("KEY", value)) // Invariant enforced
```
Also wraps fallible functions for HOF use:
```go
mustAtoi := must.Of(strconv.Atoi)
ints := slice.From(strings).ToInt(mustAtoi)
```
### value
Value-first conditional selection:
```go
// "value of CurrentTick when CurrentTick < 7, or 7"
days := value.Of(tick).When(tick < 7).Or(7)
// Lazy evaluation for expensive computations
config := value.OfCall(loadFromDB).When(useCache).Or(defaultConfig)
```
## The Familiarity Discount
A `for` loop you've seen 10,000 times feels instant to parse—but only because you've amortized the cognitive load through repetition. fluentfp expresses intent without mechanics; the simplicity is inherent, not learned. Be aware of this discount when comparing approaches.
## Further Reading
- [Full Analysis](analysis.md) - Technical deep-dive with examples
- [Methodology](methodology.md) - How claims were measured
- [Nil Safety](nil-safety.md) - The billion-dollar mistake and Go
- [Naming Functions](naming-in-hof.md) - Function naming patterns for HOF use
- [Library Comparison](comparison.md) - How fluentfp compares to alternatives
## Recent Additions
- **v0.14.0**: `value` package replaces `ternary` — value-first conditional selection
- **v0.12.0**: **BREAKING** — `MapperTo.To` renamed to `MapperTo.Map` for clarity
- **v0.8.0**: `either` package (Left/Right sum types), `ToInt32`/`ToInt64` (slice package)
- **v0.7.0**: `IfNotZero` for comparable types (option package)
- **v0.6.0**: `Fold`, `Unzip2/3/4`, `Zip`/`ZipWith` (pair package)
- **v0.5.0**: `ToFloat64`, `ToFloat32`
## License
fluentfp is licensed under the MIT License. See [LICENSE](LICENSE) for more details.