Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fn - Fluent Functional Operations for Go

A fluent, chainable library for functional-style slice operations in Go. Provides a familiar API for filtering, mapping, reducing, and transforming collections with full type safety via Go generics.

Installation

go get github.com/RobGraham/fluent

Quick Start

import "github.com/RobGraham/fluent"

// Basic filtering and chaining
activeProducts := fn.Of(products).
    Filter(func(p Product) bool { return p.Active }).
    Filter(func(p Product) bool { return p.Price > 50 }).
    Take(10).
    Collect()

// Type-changing operations use package functions
names := fn.Map(
    fn.Of(products).Filter(func(p Product) bool { return p.Active }),
    func(p Product) string { return p.Name },
).Collect()

// Deep nested filtering with FlatMap
inStockSKUs := fn.Map(
    fn.FlatMap(fn.Of(products), func(p Product) []Variant {
        return p.Variants
    }).Filter(func(v Variant) bool {
        return v.InStock
    }),
    func(v Variant) string { return v.SKU },
).Collect()

API Design Philosophy

Due to Go's type system limitation where methods cannot introduce new type parameters, this library uses a hybrid approach:

  • Same-type operations → chainable methods (.Filter(), .Take(), .Reverse())
  • Type-changing operations → package functions (fn.Map(), fn.FlatMap(), fn.Reduce())

This provides the best balance of ergonomics within Go's constraints.

Design Notes

  1. Immutable: All operations return new slices; originals are never mutated
  2. Lazy-ish: Operations chain but execute immediately (not truly lazy iterators)
  3. Type-safe: Full generic support with compile-time type checking
  4. Zero dependencies: Uses only Go standard library

Performance

This library is designed with performance in mind. Key optimizations include:

  • Pre-sized allocations: Functions like Union, Difference, and Intersection calculate expected sizes upfront to minimize map and slice reallocations during growth.
  • Early exits: Operations like Intersection short-circuit when results become empty, avoiding unnecessary iteration over remaining inputs.
  • Direct iteration: Predicates are called directly without wrapper closures where possible, reducing function call overhead.
  • Standard library leverage: Operations delegate to optimized slices and maps package functions (e.g., slices.Clone, slices.Concat) where they outperform manual implementations.

Trade-offs

Some operations have performance characteristics that vary based on your data:

  • Filter/Reject: Uses clone-then-delete rather than build-from-scratch. This provides consistent performance regardless of how many elements match, but may use more memory when very few elements pass the predicate.
  • FlatMap: Iterates twice (once to calculate total size, once to build the result) to avoid intermediate allocations. This is faster for typical struct field access but could be slower if your transform function is computationally expensive.
  • SortBy: Computes the key function during each comparison. For simple field access this is optimal, but if your key function is expensive, consider precomputing keys into a struct and sorting that instead.

For most use cases, these trade-offs favor the common path. If you're working with performance-critical code, benchmark with your actual data.

API Reference

Entry Point

fn.Of(slice []T) Slice[T]    // Wrap a slice for fluent operations
slice.Collect() []T          // Extract the final result

Chainable Methods (Same Type)

Method Description
.Filter(predicate) Keep elements matching predicate
.Reject(predicate) Remove elements matching predicate
.Take(n) First n elements
.TakeRight(n) Last n elements
.TakeWhile(predicate) Take while predicate is true
.Drop(n) Skip first n elements
.DropRight(n) Skip last n elements
.DropWhile(predicate) Drop while predicate is true
.Reverse() Reverse order
.Concat(others...) Append slices
.Append(elements...) Append elements
.Prepend(elements...) Prepend elements
.Clone() Shallow copy
.ForEach(fn) Execute for each (returns self)
.ForEachIndexed(fn) Execute with index

Terminal Methods (Return Values)

Method Return Type Description
.Collect() []T Get underlying slice
.Len() int Number of elements
.IsEmpty() bool True if empty
.First() (T, bool) First element
.Last() (T, bool) Last element
.Nth(n) (T, bool) Element at index (negative = from end)
.Find(predicate) (T, bool) First matching element
.FindLast(predicate) (T, bool) Last matching element
.FindIndex(predicate) int Index of first match (-1 if none)
.FindLastIndex(predicate) int Index of last match
.Some(predicate) bool Any element matches
.Every(predicate) bool All elements match
.None(predicate) bool No elements match
.Count(predicate) int Count of matching elements
.Partition(predicate) (Slice[T], Slice[T]) Split by predicate
.Chunk(size) [][]T Split into chunks

Package Functions (Type-Changing)

// Transform
fn.Map(s, func(T) U) Slice[U]           // Transform each element
fn.FlatMap(s, func(T) []U) Slice[U]     // Transform and flatten
fn.Flatten(s Slice[[]T]) Slice[T]       // Flatten nested slices

// Aggregate
fn.Reduce(s, initial U, func(U, T) U) U
fn.ReduceRight(s, initial, fn) U

// Group
fn.GroupBy(s, keyFn) map[K][]T          // Group by key
fn.KeyBy(s, keyFn) map[K]T              // Index by key
fn.CountBy(s, keyFn) map[K]int          // Count by key

// Sort
fn.SortBy(s, keyFn) Slice[T]            // Sort by comparable key
fn.SortByDesc(s, keyFn) Slice[T]        // Sort descending
fn.SortFunc(s, cmpFn) Slice[T]          // Custom comparator

// Unique
fn.Unique(s Slice[T]) Slice[T]          // Remove duplicates (T must be comparable)
fn.UniqueBy(s, keyFn) Slice[T]          // Unique by key function

// Set Operations
fn.Contains(s, element) bool
fn.Compact(s) Slice[T]                  // Remove zero values
fn.Difference(s, others...) Slice[T]    // Elements not in others
fn.Intersection(s, others...) Slice[T]  // Common elements
fn.Union(s, others...) Slice[T]         // All unique elements

// Aggregations
fn.Max(s) (T, bool)                     // Maximum (T must be ordered)
fn.Min(s) (T, bool)                     // Minimum
fn.Sum(s) T                             // Sum (T must be ordered)
fn.MaxBy(s, keyFn) (T, bool)            // Max by key
fn.MinBy(s, keyFn) (T, bool)            // Min by key
fn.SumBy(s, keyFn) K                    // Sum by key

// Conversion
fn.ToMap(s, keyFn, valueFn) map[K]V
fn.ToSet(s) map[T]struct{}
fn.FromMap(m) Slice[V]
fn.FromMapKeys(m) Slice[K]
fn.FromMapEntries(m) Slice[Entry[K,V]]

// Utilities
fn.Range(start, end) Slice[int]

Real-World Examples

E-Commerce: Find Low Stock Items

lowStock := fn.FlatMap(
    fn.Of(products).Filter(func(p Product) bool { return p.Active }),
    func(p Product) []InventoryAlert {
        return fn.Map(
            fn.Of(p.Variants).Filter(func(v Variant) bool {
                return v.InStock && v.Quantity < 20
            }),
            func(v Variant) InventoryAlert {
                return InventoryAlert{
                    ProductName: p.Name,
                    SKU:         v.SKU,
                    Quantity:    v.Quantity,
                }
            },
        ).Collect()
    },
).Collect()

Analytics: High-Value Customers

highValueCustomers := fn.Of(users).
    Filter(func(u User) bool { return u.Active }).
    Filter(func(u User) bool {
        total := fn.Reduce(fn.Of(u.Orders), 0.0, func(acc float64, o Order) float64 {
            return acc + o.Total
        })
        return total > 100
    }).
    Collect()

Deep Nesting: Line Items from Completed Orders

completedLineItems := fn.FlatMap(
    fn.FlatMap(
        fn.Of(users).Filter(func(u User) bool { return u.Active }),
        func(u User) []Order { return u.Orders },
    ).Filter(func(o Order) bool { return o.Status == "completed" }),
    func(o Order) []LineItem { return o.LineItems },
).Collect()

Search with Sorting

searchResults := fn.SortBy(
    fn.Of(products).
        Filter(func(p Product) bool { return p.Active }).
        Filter(func(p Product) bool {
            return fn.Of(p.Tags).Some(func(tag string) bool {
                return strings.Contains(tag, query)
            })
        }),
    func(p Product) float64 { return p.Price },
).Take(20).Collect()

Grouping and Aggregation

// Group products by category
byCategory := fn.GroupBy(fn.Of(products), func(p Product) string {
    return p.Category
})

// Index users by ID for O(1) lookup
userIndex := fn.KeyBy(fn.Of(users), func(u User) int {
    return u.ID
})

// Count products per category
categoryCounts := fn.CountBy(fn.Of(products), func(p Product) string {
    return p.Category
})

Key Patterns

Pattern 1: Filter → Map → Collect

names := fn.Map(
    fn.Of(products).Filter(func(p Product) bool { return p.Active }),
    func(p Product) string { return p.Name },
).Collect()

Pattern 2: Nested Structures with FlatMap

// products → variants → images
allImages := fn.FlatMap(
    fn.FlatMap(fn.Of(products), func(p Product) []Variant { return p.Variants }),
    func(v Variant) []Image { return v.Images },
).Collect()

Pattern 3: Partition for Split Decisions

active, inactive := fn.Of(products).Partition(func(p Product) bool {
    return p.Active
})

Pattern 4: Reduce for Custom Aggregation

totalRevenue := fn.Reduce(
    fn.Of(orders),
    0.0,
    func(acc float64, o Order) float64 { return acc + o.Total },
)

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages