fluentfp

module
v0.15.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 12, 2026 License: MIT

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 for complete API documentation.

Quick Start

go get github.com/binaryphile/fluentfp
// 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.

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.

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.

// 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 Collection transforms KeepIf, RemoveIf, Fold, ToString
option Nil safety Of, Get, Or, IfNotZero, IfNotNil
either Sum types Left, Right, Fold, Map
must Fallible funcs → HOF args Get, BeNil, Of
value Conditional value selection Of().When().Or()
pair Zip slices Zip, ZipWith
lof Lower-order function wrappers Len, Println, StringLen

Installation

go get github.com/binaryphile/fluentfp
import "github.com/binaryphile/fluentfp/slice"
import "github.com/binaryphile/fluentfp/option"

Package Highlights

slice

Fluent collection operations with method chaining:

// 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:

// 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:

// 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()):

_ = os.Setenv("KEY", value)           // Silent corruption if error
must.BeNil(os.Setenv("KEY", value))   // Invariant enforced

Also wraps fallible functions for HOF use:

mustAtoi := must.Of(strconv.Atoi)
ints := slice.From(strings).ToInt(mustAtoi)
value

Value-first conditional selection:

// "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

Recent Additions

  • v0.14.0: value package replaces ternary — value-first conditional selection
  • v0.12.0: BREAKINGMapperTo.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 for more details.

Directories

Path Synopsis
Package either provides a sum type representing a value of one of two types.
Package either provides a sum type representing a value of one of two types.
Package main demonstrates the value package for conditional value selection.
Package main demonstrates the value package for conditional value selection.
code-shape command
exp
Package lof provides utility functions for functional programming.
Package lof provides utility functions for functional programming.
Package must provides functions to panic if a condition is not met.
Package must provides functions to panic if a condition is not met.
Package option provides types and functions to work with optional values.
Package option provides types and functions to work with optional values.
Package slice provides fluent slice types that can chain functional collection operations.
Package slice provides fluent slice types that can chain functional collection operations.
tuple
pair
Package pair provides tuple types and functions for working with pairs of values.
Package pair provides tuple types and functions for working with pairs of values.
Package value provides value-first conditional selection.
Package value provides value-first conditional selection.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL