fn

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Dec 21, 2025 License: MIT Imports: 3 Imported by: 0

README

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

Documentation

Overview

Package fn provides fluent, functional-style operations for slices in Go. It offers a chainable API for filtering, mapping, and transforming collections.

Basic usage:

result := fn.Of(products).
    Filter(func(p Product) bool { return p.Active }).
    Take(10).
    Collect()

For type-changing operations (Map, FlatMap, Reduce, etc.), use the package-level functions:

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

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Contains

func Contains[T comparable](s Slice[T], element T) bool

Contains returns true if the slice contains the specified element.

func CountBy

func CountBy[T any, K comparable](s Slice[T], keyFn func(T) K) map[K]int

CountBy counts elements by a key function. Returns a map of counts per key.

Example:

counts := fn.CountBy(fn.Of(products), func(p Product) string {
    return p.Category
})
// Result: map[string]int{"shoes": 5, "shirts": 10}

func GroupBy

func GroupBy[T any, K comparable](s Slice[T], keyFn func(T) K) map[K][]T

GroupBy groups elements by a key function. Returns a map where keys are the result of keyFn and values are slices of elements.

Example:

byCategory := fn.GroupBy(fn.Of(products), func(p Product) string {
    return p.Category
})
// Result: map[string][]Product{"shoes": [...], "shirts": [...]}

func IndexBy

func IndexBy[T any, K comparable](s Slice[T], keyFn func(T) K) map[K]T

IndexBy is an alias for KeyBy.

func KeyBy

func KeyBy[T any, K comparable](s Slice[T], keyFn func(T) K) map[K]T

KeyBy creates a map keyed by the result of keyFn. If multiple elements have the same key, the last one wins.

Example:

byID := fn.KeyBy(fn.Of(users), func(u User) int { return u.ID })
// Result: map[int]User{1: user1, 2: user2, ...}

func Max

func Max[T cmp.Ordered](s Slice[T]) (T, bool)

Max returns the maximum element and true, or zero and false if empty.

func MaxBy

func MaxBy[T any, K cmp.Ordered](s Slice[T], keyFn func(T) K) (T, bool)

MaxBy returns the element with the maximum value of the key function. Returns zero value and false if slice is empty.

func Min

func Min[T cmp.Ordered](s Slice[T]) (T, bool)

Min returns the minimum element and true, or zero and false if empty.

func MinBy

func MinBy[T any, K cmp.Ordered](s Slice[T], keyFn func(T) K) (T, bool)

MinBy returns the element with the minimum value of the key function. Returns zero value and false if slice is empty.

func Reduce

func Reduce[T, U any](s Slice[T], initial U, fn func(U, T) U) U

Reduce reduces the slice to a single value using an accumulator function.

Example:

sum := fn.Reduce(fn.Of(numbers), 0, func(acc, n int) int { return acc + n })

func ReduceRight

func ReduceRight[T, U any](s Slice[T], initial U, fn func(U, T) U) U

ReduceRight reduces the slice from right to left.

func Sum

func Sum[T cmp.Ordered](s Slice[T]) T

Sum returns the sum of all elements (for numeric slices).

func SumBy

func SumBy[T any, K cmp.Ordered](s Slice[T], keyFn func(T) K) K

SumBy returns the sum of values returned by the key function.

func ToMap

func ToMap[T any, K comparable, V any](s Slice[T], keyFn func(T) K, valueFn func(T) V) map[K]V

ToMap converts a slice to a map using key and value functions.

Example:

userMap := fn.ToMap(fn.Of(users),
    func(u User) int { return u.ID },
    func(u User) string { return u.Name },
)
// Result: map[int]string{1: "Alice", 2: "Bob"}

func ToSet

func ToSet[T comparable](s Slice[T]) map[T]struct{}

ToSet converts a slice to a set (map[T]struct{}). Useful for O(1) membership checks.

Example:

idSet := fn.ToSet(fn.Of([]int{1, 2, 3}))
_, exists := idSet[2] // true

Types

type Entry

type Entry[K comparable, V any] struct {
	Key   K
	Value V
}

FromMapEntries creates a Slice of key-value pairs from a map.

type Slice

type Slice[T any] struct {
	// contains filtered or unexported fields
}

Slice wraps a slice to provide fluent, chainable operations. All methods return new slices and never mutate the original.

func Compact

func Compact[T comparable](s Slice[T]) Slice[T]

Compact removes zero values from the slice.

Example:

compact := fn.Compact(fn.Of([]string{"", "a", "", "b"})).Collect()
// Result: []string{"a", "b"}

func Difference

func Difference[T comparable](s Slice[T], others ...[]T) Slice[T]

Difference returns elements in s that are not in others.

Example:

diff := fn.Difference(fn.Of([]int{1, 2, 3}), []int{2, 3, 4}).Collect()
// Result: []int{1}

func FlatMap

func FlatMap[T, U any](s Slice[T], fn func(T) []U) Slice[U]

FlatMap transforms each element into a slice and flattens the result. Essential for drilling into nested slices.

Example:

// Get all variants from all products
variants := fn.FlatMap(fn.Of(products), func(p Product) []Variant {
    return p.Variants
}).Filter(func(v Variant) bool { return v.InStock }).Collect()

func Flatten

func Flatten[T any](s Slice[[]T]) Slice[T]

Flatten flattens a slice of slices into a single slice.

Example:

flattened := fn.Flatten(fn.Of([][]int{{1, 2}, {3, 4}})).Collect()
// Result: []int{1, 2, 3, 4}

func FromMap

func FromMap[K comparable, V any](m map[K]V) Slice[V]

FromMap creates a Slice from map values.

Example:

users := fn.FromMap(userMap).Filter(...).Collect()

func FromMapEntries

func FromMapEntries[K comparable, V any](m map[K]V) Slice[Entry[K, V]]

func FromMapKeys

func FromMapKeys[K comparable, V any](m map[K]V) Slice[K]

FromMapKeys creates a Slice from map keys.

func Intersection

func Intersection[T comparable](s Slice[T], others ...[]T) Slice[T]

Intersection returns elements that exist in both s and all others.

Example:

common := fn.Intersection(fn.Of([]int{1, 2, 3}), []int{2, 3, 4}).Collect()
// Result: []int{2, 3}

func Map

func Map[T, U any](s Slice[T], fn func(T) U) Slice[U]

Map transforms each element using the provided function. Returns a new Slice of the transformed type.

Example:

names := fn.Map(fn.Of(users), func(u User) string { return u.Name }).Collect()

func Of

func Of[T any](data []T) Slice[T]

Of creates a new Slice wrapper from an existing slice. This is the entry point for all fluent operations.

func Range

func Range(start, end int) Slice[int]

Range creates a Slice of integers from start to end (exclusive).

Example:

nums := fn.Range(0, 5).Collect() // []int{0, 1, 2, 3, 4}

func SortBy

func SortBy[T any, K cmp.Ordered](s Slice[T], keyFn func(T) K) Slice[T]

SortBy returns a new Slice sorted by a comparable key.

Example:

sorted := fn.SortBy(fn.Of(users), func(u User) string {
    return u.Name
}).Collect()

func SortByDesc

func SortByDesc[T any, K cmp.Ordered](s Slice[T], keyFn func(T) K) Slice[T]

SortByDesc returns a new Slice sorted by a comparable key in descending order.

func SortFunc

func SortFunc[T any](s Slice[T], cmpFn func(a, b T) int) Slice[T]

SortFunc returns a new Slice sorted using a custom comparison function. The comparison function should return a negative number when a < b, zero when a == b, and a positive number when a > b.

func Union

func Union[T comparable](s Slice[T], others ...[]T) Slice[T]

Union returns all unique elements from s and others combined.

Example:

all := fn.Union(fn.Of([]int{1, 2}), []int{2, 3}).Collect()
// Result: []int{1, 2, 3}

func Unique

func Unique[T comparable](s Slice[T]) Slice[T]

Unique returns a new Slice with duplicate elements removed. Uses a map for O(1) lookups, so T must be comparable.

Example:

unique := fn.Unique(fn.Of([]int{1, 2, 2, 3, 3, 3})).Collect()
// Result: []int{1, 2, 3}

func UniqueBy

func UniqueBy[T any, K comparable](s Slice[T], keyFn func(T) K) Slice[T]

UniqueBy returns a new Slice with duplicates removed based on a key function.

Example:

// Unique users by email
unique := fn.UniqueBy(fn.Of(users), func(u User) string {
    return u.Email
}).Collect()

func (Slice[T]) Append

func (s Slice[T]) Append(elements ...T) Slice[T]

Append returns a new Slice with additional elements appended.

func (Slice[T]) Chunk

func (s Slice[T]) Chunk(size int) [][]T

Chunk splits the slice into groups of the specified size. The last chunk may have fewer elements.

func (Slice[T]) Clone

func (s Slice[T]) Clone() Slice[T]

Clone returns a shallow copy of the Slice.

func (Slice[T]) Collect

func (s Slice[T]) Collect() []T

Collect returns the underlying slice. Use this to extract the final result after chaining operations.

func (Slice[T]) Concat

func (s Slice[T]) Concat(others ...[]T) Slice[T]

Concat returns a new Slice with additional slices appended.

func (Slice[T]) Count

func (s Slice[T]) Count(predicate func(T) bool) int

Count returns the number of elements that satisfy the predicate.

func (Slice[T]) Drop

func (s Slice[T]) Drop(n int) Slice[T]

Drop returns a new Slice without the first n elements. If n >= len, returns empty slice. If n <= 0, returns all elements.

func (Slice[T]) DropRight

func (s Slice[T]) DropRight(n int) Slice[T]

DropRight returns a new Slice without the last n elements.

func (Slice[T]) DropWhile

func (s Slice[T]) DropWhile(predicate func(T) bool) Slice[T]

DropWhile returns elements after dropping from the beginning while predicate returns true.

func (Slice[T]) Every

func (s Slice[T]) Every(predicate func(T) bool) bool

Every returns true if all elements satisfy the predicate. Returns true for empty slices.

func (Slice[T]) Filter

func (s Slice[T]) Filter(predicate func(T) bool) Slice[T]

Filter returns a new Slice containing only elements that satisfy the predicate.

func (Slice[T]) Find

func (s Slice[T]) Find(predicate func(T) bool) (T, bool)

Find returns the first element that satisfies the predicate and true, or the zero value and false if no element matches.

func (Slice[T]) FindIndex

func (s Slice[T]) FindIndex(predicate func(T) bool) int

FindIndex returns the index of the first element that satisfies the predicate, or -1 if no element matches.

func (Slice[T]) FindLast

func (s Slice[T]) FindLast(predicate func(T) bool) (T, bool)

FindLast returns the last element that satisfies the predicate and true, or the zero value and false if no element matches.

func (Slice[T]) FindLastIndex

func (s Slice[T]) FindLastIndex(predicate func(T) bool) int

FindLastIndex returns the index of the last element that satisfies the predicate, or -1 if no element matches.

func (Slice[T]) First

func (s Slice[T]) First() (T, bool)

First returns the first element and true, or zero value and false if empty.

func (Slice[T]) ForEach

func (s Slice[T]) ForEach(fn func(T)) Slice[T]

ForEach executes a function for each element. Returns the same Slice for chaining.

func (Slice[T]) ForEachIndexed

func (s Slice[T]) ForEachIndexed(fn func(int, T)) Slice[T]

ForEachIndexed executes a function for each element with its index. Returns the same Slice for chaining.

func (Slice[T]) IsEmpty

func (s Slice[T]) IsEmpty() bool

IsEmpty returns true if the slice has no elements.

func (Slice[T]) Last

func (s Slice[T]) Last() (T, bool)

Last returns the last element and true, or zero value and false if empty.

func (Slice[T]) Len

func (s Slice[T]) Len() int

Len returns the number of elements in the slice.

func (Slice[T]) None

func (s Slice[T]) None(predicate func(T) bool) bool

None returns true if no elements satisfy the predicate. Returns true for empty slices.

func (Slice[T]) Nth

func (s Slice[T]) Nth(n int) (T, bool)

Nth returns the element at index n and true, or zero value and false if out of bounds. Negative indices count from the end (-1 is the last element).

func (Slice[T]) Partition

func (s Slice[T]) Partition(predicate func(T) bool) (Slice[T], Slice[T])

Partition splits elements into two slices based on the predicate. First slice contains elements where predicate returns true, second slice contains elements where predicate returns false.

func (Slice[T]) Prepend

func (s Slice[T]) Prepend(elements ...T) Slice[T]

Prepend returns a new Slice with elements prepended.

func (Slice[T]) Reject

func (s Slice[T]) Reject(predicate func(T) bool) Slice[T]

Reject returns a new Slice containing only elements that do NOT satisfy the predicate. Opposite of Filter.

func (Slice[T]) Reverse

func (s Slice[T]) Reverse() Slice[T]

Reverse returns a new Slice with elements in reverse order.

func (Slice[T]) Some

func (s Slice[T]) Some(predicate func(T) bool) bool

Some returns true if at least one element satisfies the predicate.

func (Slice[T]) Take

func (s Slice[T]) Take(n int) Slice[T]

Take returns a new Slice with the first n elements. If n > len, returns all elements. If n <= 0, returns empty slice.

func (Slice[T]) TakeRight

func (s Slice[T]) TakeRight(n int) Slice[T]

TakeRight returns a new Slice with the last n elements.

func (Slice[T]) TakeWhile

func (s Slice[T]) TakeWhile(predicate func(T) bool) Slice[T]

TakeWhile returns elements from the beginning while predicate returns true.

Jump to

Keyboard shortcuts

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