fluentfp

module
v0.42.0 Latest Latest
Warning

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

Go to latest
Published: Mar 17, 2026 License: MIT

README

fluentfp

Fluent functional programming for Go.

A thin abstraction that removes common loop-scaffolding bugs from Go. Chain type-safe operations on slices, options, and sum types — no loop scaffolding, no intermediate variables.

See pkg.go.dev for API docs and the showcase for 16 before/after rewrites from real GitHub projects.

Zero reflection. Zero global state. Zero build tags.

Quick Start

Requires Go 1.26+.

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

// Before: scaffolding around one line of intent
var names []string                         // state
for _, u := range users {                  // iteration
    if u.IsActive() {                      // predicate
        names = append(names, u.Name())    // accumulation
    }
}

// After: intent only
names := slice.From(users).KeepIf(User.IsActive).ToString(User.Name)

That's a fluent chain — each step returns a value you can call the next method on, so the whole pipeline reads as a single expression: filter, then transform.

Method Expressions

Go lets you reference a method by its type name, creating a function value where the receiver becomes the first argument:

func (u User) IsActive() bool  // method
func(User) bool                // method expression: User.IsActive

KeepIf expects func(T) boolUser.IsActive is exactly that:

names := slice.From(users).KeepIf(User.IsActive).ToString(User.Name)

Without method expressions, every predicate needs a wrapper: func(u User) bool { return u.IsActive() }.

For []*User slices, the method expression is (*User).IsActive.

See naming patterns for when to use method expressions vs named functions vs closures.

What It Looks Like

The showcase has 16 more, including Sort, Trim, and Map-to-Slice.

Conditional Struct Fields

Go struct literals let you build and return a value in one statement — fluentfp keeps it that way when fields are conditional.

BeforeAfter
var level string
if overdue {
    level = "critical"
} else {
    level = "info"
}
var icon string
if overdue {
    icon = "!"
} else {
    icon = "✓"
}
return Alert{
    Message: msg,
    Level:   level,
    Icon:    icon,
}
return Alert{
    Message: msg,
    Level:   option.When(overdue, "critical").Or("info"),
    Icon:    option.When(overdue, "!").Or("✓"),
}

Go has no inline conditional expression. option.When fills that gap — each field resolves in place, so the struct literal stays a single statement. From hashicorp/consul.

Bounded Concurrent Requests

Fetch weather for a list of cities with at most 10 simultaneous goroutines.

Before — errgroup (21 lines)After (1 line)
func Cities(ctx context.Context, cities ...string) ([]*Info, error) {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10)
    res := make([]*Info, len(cities))
    for i, city := range cities {
        g.Go(func() error {
            info, err := City(ctx, city)
            if err != nil {
                return err
            }
            res[i] = info
            return nil
        })
    }
    if err := g.Wait(); err != nil {
        return nil, err
    }
    return res, nil
}
func Cities(ctx context.Context, cities ...string) ([]*Info, error) {
    return slice.FanOutAll(ctx, 10, cities, City)
}

FanOutAll is all-or-nothing: on first error it cancels remaining work and returns that error. City passes directly — no wrapper needed. Panics in callbacks are recovered as *rslt.PanicError with stack trace.

When you need per-item outcomes instead of all-or-nothing, use FanOut:

results := slice.FanOut(ctx, 10, cities, City)
infos, errs := rslt.CollectOkAndErr(results)  // gather successes and failures separately

From the errgroup pattern.

Why fluentfp

Type-safe end-to-end. go-linq gives you []any back — cast it and hope you got the type right. lo requires func(T, int) callbacks, so every stdlib function needs a wrapper to discard the unused index. fluentfp uses generics throughout: Mapper[T] is []T with methods. If it compiles, you avoid a class of cast, index, and callback-shape mistakes.

Fewer places for bugs to hide. No index means no off-by-one in a predicate. No loop variable means no shadowing. No accumulator means no forgetting to initialize one. These are the loop-scaffolding bug classes that code review catches regularly — fluentfp removes the scaffolding where they live.

Works with Go, not against it. Mappers are slices — callers of your functions don't need to import fluentfp. Options use comma-ok (.Get() (T, bool)), the same pattern as map lookups and type assertions. either.Fold gives you exhaustive dispatch the compiler enforces — miss a branch and it doesn't compile. must.BeNil makes invariant enforcement explicit. Mutation, channels, and hot paths stay as loops.

Interchangeable Types

Mapper[T] is defined as type Mapper[T any] []T — a defined type, not a wrapper. []T and Mapper[T] convert implicitly in either direction, so you choose how much to expose:

// Public API — hide the dependency. Callers never see fluentfp types.
func ActiveNames(users []User) []string {
    return slice.From(users).KeepIf(User.IsActive).ToString(User.Name)
}

// Internal — embrace it. Accepting Mapper saves From() calls across a chain of helpers.
func transform(users slice.Mapper[User]) slice.Mapper[User] {
    return users.KeepIf(User.IsActive).Convert(User.Normalize)
}

The public pattern keeps fluentfp as an implementation detail — callers don't import it, and its types don't appear in intellisense. Internal code can pass Mappers between helpers to avoid repeated From() wrapping.

Performance

A single chain step (filter OR map) matches a hand-written loop — slice.From is a zero-cost type conversion, and each operation pre-allocates with make([]T, 0, len(input)).

Multi-step chains pay one allocation per stage. A chain that filters then maps makes two passes and two allocations where a hand-written loop can fuse them into one. In benchmarks (1000 elements), a two-step chain runs ~2.5× slower than the fused equivalent.

If you're counting nanoseconds in a hot path, fuse it in a loop. Most loops aren't hot paths — they're scaffolding that fluentfp eliminates.

Measurable Impact

Codebase Type Code Reduction Complexity Reduction
Mixed (typical) 12% 26%
Pure pipeline 47% 95%

Individual loops see up to 6x line reduction. Codebase-wide averages are lower because not every line is a loop. Complexity measured via scc. See methodology.

When to Use Loops

fluentfp replaces mechanical loops — iteration scaffolding around a predicate and a transform. It doesn't try to replace loops that do structural work:

// Mutation in place — fluentfp returns new slices, but elements are shared (shallow copy)
for i := range items {
    if items[i].ID == target {
        items[i].Status = "done"
        break
    }
}

// Channel consumption — direct range is simplest for straightforward use
for msg := range ch {
    handle(msg)
}

// Bridge to fluentfp when you want operators on a channel
seq.FromChannel(ctx, ch).KeepIf(valid).Take(10).Each(handle)

// Complex control flow — early return, labeled break
for _, item := range items {
    if item.IsTerminal() {
        return item.Result()
    }
}

// Hot paths — fuse filter+map into one pass when nanoseconds matter
out := make([]R, 0, len(input))
for _, v := range input {
    if keep(v) {
        out = append(out, transform(v))
    }
}

Packages

Packages are independent — import one or all.

Package Purpose Key Functions
slice Collection transforms KeepIf, RemoveIf, Fold, FanOutAll
kv Map transforms KeepIf, MapValues, Map, Values
option Optional values + conditionals Of, When, WhenCall, Or, NonZero
either Sum types Left, Right, Fold, Convert, FlatMap
rslt Typed error handling Ok, Err, CollectAll, CollectOk
must Invariant enforcement Get, BeNil, Of
stream Lazy memoized sequences Generate, Unfold, Take, Collect
seq Fluent iter.Seq chains From, KeepIf, Take, Collect
heap Persistent priority queue New, Insert, Pop, Collect
hof Function combinators Pipe, Bind, Throttle, Retry, Debouncer
pair Zip slices Zip, ZipWith
combo Combinatorial constructions CartesianProduct, Combinations, PowerSet
memo Memoization Of, Fn, FnErr, NewLRU
toc Constrained stage runner with pipeline composition Start, Pipe, NewBatcher, NewWeightedBatcher, Submit, CloseInput, Wait
lof Lower-order function wrappers Len, Println, Identity, Inc

Package Highlights

rslt — typed error handling as values:

r := rslt.Of(strconv.Atoi(input))  // wrap (int, error) → Result[int]
port := r.Or(8080)                   // value or default

seq — fluent chains on Go's iter.Seq:

active := seq.FromIter(maps.Keys(configs)).KeepIf(isActive).Collect()

stream — lazy memoized sequences:

naturals := stream.Generate(0, lof.Inc)
first10Squares := stream.Map(naturals, square).Take(10).Collect()

Further Reading

See CHANGELOG for version history.

License

fluentfp is licensed under the MIT License. See LICENSE for more details.

Directories

Path Synopsis
Package combo provides combinatorial constructions: Cartesian products, permutations, combinations, and power sets.
Package combo provides combinatorial constructions: Cartesian products, permutations, combinations, and power sets.
Package ctxval provides typed helpers for storing and retrieving request-scoped values in context.Context.
Package ctxval provides typed helpers for storing and retrieving request-scoped values in context.Context.
Package either provides a sum type representing either a Left or a Right value.
Package either provides a sum type representing either a Left or a Right value.
Package heap provides a persistent (immutable) priority queue backed by a pairing heap, parameterized by a comparator function.
Package heap provides a persistent (immutable) priority queue backed by a pairing heap, parameterized by a comparator function.
Package hof provides function combinators for composition, partial application, independent application, concurrency control, side-effect wrapping, and call coalescing.
Package hof provides function combinators for composition, partial application, independent application, concurrency control, side-effect wrapping, and call coalescing.
internal
base
Package base defines the core collection types and their methods.
Package base defines the core collection types and their methods.
Package kv provides fluent operations on Go maps (key-value collections).
Package kv provides fluent operations on Go maps (key-value collections).
Package lof provides utility functions for functional programming.
Package lof provides utility functions for functional programming.
Package memo provides memoization primitives: lazy zero-arg evaluation, keyed function caching, and pluggable cache strategies.
Package memo provides memoization primitives: lazy zero-arg evaluation, keyed function caching, and pluggable cache strategies.
Package must provides panic-on-error helpers for enforcing invariants.
Package must provides panic-on-error helpers for enforcing invariants.
Package option provides types and functions to work with optional values.
Package option provides types and functions to work with optional values.
Package rslt provides a Result type for operations that may fail.
Package rslt provides a Result type for operations that may fail.
Package seq provides lazy iterator operations on iter.Seq[T] with method chaining.
Package seq provides lazy iterator operations on iter.Seq[T] with method chaining.
Package slice provides fluent slice types that can chain functional collection operations.
Package slice provides fluent slice types that can chain functional collection operations.
Package stream provides lazy, memoized, persistent sequences.
Package stream provides lazy, memoized, persistent sequences.
Package toc provides a constrained stage runner inspired by Drum-Buffer-Rope (Theory of Constraints), with pipeline composition via Pipe and NewBatcher.
Package toc provides a constrained stage runner inspired by Drum-Buffer-Rope (Theory of Constraints), with pipeline composition via Pipe and NewBatcher.
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 web provides JSON HTTP adapter composition for net/http handlers.
Package web provides JSON HTTP adapter composition for net/http handlers.

Jump to

Keyboard shortcuts

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