slice

package
v0.38.0 Latest Latest
Warning

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

Go to latest
Published: Mar 6, 2026 License: MIT Imports: 4 Imported by: 0

README

slice

Replace loop scaffolding with type-safe collection chains.

  • InterchangeableMapper[T] is []T. Index, range, append, len all work. Pass to or return from any function expecting []T.
  • Generics — 100% type-safe. No any, no reflection, no type assertions.
  • Method expressions — pass User.IsActive directly. No wrapper closures.
  • Comma-okFind, IndexWhere return option with .Get()(value, ok).
// Before: 3 lines of scaffolding, 2 closing braces, 1 line of intent
var names []string
for _, u := range users {
    if u.IsActive() {
        names = append(names, u.Name)
    }
}

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

Six lines become one.

What It Looks Like

// Ranking
top5 := slice.SortByDesc(players, Player.Score).Take(5)
// Tag filtering (allowlist semantics — empty filter matches all)
if !slice.String(m.Tags).Matches(filter.Tags) {
    continue
}
// Cross-type mapping (both types inferred)
users := slice.Map(ids, FetchUser)
// Multi-field extraction in one pass
prices, quantities, discounts, taxes := slice.Unzip4(orders,
    Order.Price, Order.Quantity, Order.Discount, Order.Tax,
)

Order.Price is a method expression — Go turns the method into a func(Order) float64, which is exactly what Unzip expects. This works when your types have accessor methods. Without them, named functions work in the same position:

// With accessor methods
prices, qtys := slice.Unzip2(orders, Order.Price, Order.Quantity)

// Without — named functions instead
getPrice := func(o Order) float64 { return o.Price() }
getQty := func(o Order) int { return o.Qty() }
prices, qtys := slice.Unzip2(orders, getPrice, getQty)

Method expressions read as intent at the call site — Order.Price vs reading a function body. They pay off when multiple sites extract the same field. For one-off use, named functions avoid the extra method.

// Set construction for O(1) lookup — works with any comparable type
allowed := slice.ToSet(cfg.AllowedRoles)
if allowed[user.Role] {
    grant(user)
}
// Expand each department into its members — one-to-many, then flatten
allEmployees := slice.From(departments).FlatMap(Department.Employees)
// Reduce to map
byMAC := slice.Fold(devices, make(map[string]Device), addDevice)
// Number items starting from 1 — state is the counter, output is the labeled string
// number returns the next counter and a formatted label.
number := func(n int, item Item) (int, string) {
    return n + 1, fmt.Sprintf("%d. %s", n, item.Name)
}
_, numbered := slice.MapAccum(items, 1, number)
// ["1. Apples", "2. Bread", "3. Milk"]

It's Just a Slice

Mapper[T] is []T. Use it anywhere you'd use a slice:

func activeNames(users []User) []string {
    names := slice.From(users).KeepIf(User.IsActive).ToString(User.Name)
    names.Each(lof.Println)
    return names  // return as []string — no conversion needed
}
result := slice.From(users).KeepIf(User.IsActive)
fmt.Println(result[0])         // index
fmt.Println(len(result))       // len
result = append(result, extra) // append
for _, u := range result {     // range
    process(u)
}
  • Keep []T in your function signatures, not Mapper[T] — use From() at the point of use. This keeps fluentfp as an implementation detail; callers don't need to import it.
  • From() is a zero-cost type conversion — no array copy. The Go spec guarantees that converting between types with identical underlying types only changes the type, not the representation. The 24-byte slice header (pointer, length, capacity) is shared; the backing array is the same. (append to either may mutate the other if capacity remains.)
  • Mutation boundaries: Most operations (KeepIf, Convert, ToString, etc.) allocate fresh slices — the result is independent of the input. View operations (From alone, Take, TakeLast, Chunk) share the backing array. Use .Clone() at boundaries where shared state could be mutated. See design.md § Boundaries and Defensive Copying for practical guidance.
  • Nil-safe: From(nil).KeepIf(...).ToString(...) returns an empty slice — Go's range over nil is zero iterations

Other Go FP libraries can't do this:

  • go-linq, fuego: 6+ lines of []any[]string with type assertions to get results out
  • gofp: conversion loops on both ends — []User[]any in, []any[]string out

See comparison for the full library comparison.

Operations

From creates Mapper[T]. For cross-type mapping, prefer the standalone Map(ts, fn) which infers all types and returns Mapper[R] for chaining. MapTo[R] creates MapperTo[R,T] for the narrow case where you filter before cross-type mapping: MapTo[R](ts).KeepIf(pred).Map(fn). String ([]string), Int ([]int), and Float64 ([]float64) are separate defined types with additional methods.

  • Filter: KeepIf, RemoveIf, Take, TakeLast, Compact
  • Search: Find, IndexWhere, FindAs, Any, Every, None, First, Single, Contains, ContainsAny, Matches (String)
  • Transform: Convert, FlatMap, Map (MapperTo), Reverse, ToString, ToInt, other To*, Clone, Unique (String), UniqueBy, SortBy, SortByDesc
  • Aggregate: Fold, MapAccum, Len, Max (Int, Float64), Min (Int, Float64), Sum (Int, Float64), ToSet, ToSetBy, Each, Unzip2/3/4
  • Parallel: ParallelMap, ParallelKeepIf, ParallelEach

Fold, not Reduce: Fold takes an initial value and allows the return type to differ from the element type (func(R, T) R). Reduce conventionally implies no initial value and same-type accumulation. The name matches the semantics.

Parallel Operations

Same API, concurrent execution. The pure-function contract (no shared state in predicates or transforms) makes parallelism safe by construction.

// CPU-bound transform — 4x speedup at 10k elements
scores := slice.ParallelMap(users, runtime.GOMAXPROCS(0), ComputeScore)

// Parallel filter chains naturally into sequential operations
active := slice.From(users).ParallelKeepIf(4, User.IsActive).ToString(User.Name)

Parallel overhead only pays off when fn does meaningful work per element. Pure field access won't benefit — CPU-bound transforms and I/O-bound operations will. Benchmarks:

Operation Work 10k elements Speedup
ParallelMap trivial (n*2) ~2x slower
ParallelMap CPU-bound (50 sin/cos) ~5x faster yes
ParallelKeepIf trivial (n%2) ~4x slower
ParallelKeepIf CPU-bound ~4x faster yes

Run go test -bench=BenchmarkParallel -benchmem ./slice/ for numbers on your hardware.

Edge cases: workers <= 0 panics, workers == 1 runs sequentially (no goroutine overhead), workers > len clamps, nil/empty input returns empty (not nil).

See pkg.go.dev for complete API documentation, the main README for installation and performance characteristics, and the showcase for real-world rewrites.

Documentation

Overview

Package slice provides fluent slice types that can chain functional collection operations.

Types are defined in internal/base and aliased here. All methods are available through the aliases.

Mapper[T] is a fluent slice that can chain operations like ToString (map), KeepIf (filter), etc.

MapperTo[T, R] is a fluent slice with one additional method, MapTo, for mapping to a specified type R. If you don't need to map to an arbitrary type, use Mapper instead.

Entries[K, V] is a fluent map type for chaining map operations.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Asc added in v0.35.0

func Asc[T any, S cmp.Ordered](key func(T) S) func(T, T) int

Asc builds an ascending comparator from a key extractor.

func Chunk added in v0.28.0

func Chunk[T any](ts []T, size int) [][]T

Chunk splits ts into sub-slices of at most size elements. The last chunk may have fewer than size elements. Panics if size <= 0.

func Contains added in v0.28.0

func Contains[T comparable](ts []T, target T) bool

Contains returns true if ts contains target.

func Desc added in v0.35.0

func Desc[T any, S cmp.Ordered](key func(T) S) func(T, T) int

Desc builds a descending comparator from a key extractor.

func FindAs added in v0.21.0

func FindAs[R, T any](ts []T) option.Option[R]

FindAs returns the first element that type-asserts to R, or not-ok if none match. Useful for finding a specific concrete type in a slice of interfaces.

func Fold added in v0.6.0

func Fold[T, R any](ts []T, initial R, fn func(R, T) R) R

Fold reduces a slice to a single value by applying fn to each element. It starts with initial and applies fn(accumulator, element) for each element from left to right. Returns initial if the slice is empty.

func KeyBy added in v0.38.0

func KeyBy[T any, K comparable](ts []T, fn func(T) K) map[K]T

KeyBy returns a map keyed by fn(element) → element. If multiple elements produce the same key, the last one wins.

func Partition added in v0.36.0

func Partition[T any](ts []T, fn func(T) bool) (Mapper[T], Mapper[T])

Partition splits ts into two slices: elements where fn returns true, and elements where it returns false. Single pass. Both results are independent slices.

func ToSet added in v0.23.0

func ToSet[T comparable](ts []T) map[T]bool

ToSet returns a map with each element as a key set to true. Requires comparable elements.

func ToSetBy added in v0.28.0

func ToSetBy[T any, K comparable](ts []T, fn func(T) K) map[K]bool

ToSetBy returns a map with each element's key (extracted by fn) set to true. Useful when elements aren't directly comparable but have a comparable key field.

func Unzip2 added in v0.6.0

func Unzip2[T, A, B any](ts []T, fa func(T) A, fb func(T) B) (Mapper[A], Mapper[B])

Unzip2 extracts two slices from ts in a single pass by applying the extraction functions. This is more efficient than calling two separate mapping operations when you need multiple fields.

func Unzip3 added in v0.6.0

func Unzip3[T, A, B, C any](ts []T, fa func(T) A, fb func(T) B, fc func(T) C) (Mapper[A], Mapper[B], Mapper[C])

Unzip3 extracts three slices from ts in a single pass by applying the extraction functions.

func Unzip4 added in v0.6.0

func Unzip4[T, A, B, C, D any](ts []T, fa func(T) A, fb func(T) B, fc func(T) C, fd func(T) D) (Mapper[A], Mapper[B], Mapper[C], Mapper[D])

Unzip4 extracts four slices from ts in a single pass by applying the extraction functions.

Types

type Any

type Any = Mapper[any]

Convenience aliases for common Mapper instantiations.

type Bool

type Bool = Mapper[bool]

type Byte

type Byte = Mapper[byte]

type Entries added in v0.35.0

type Entries[K comparable, V any] = base.Entries[K, V]

type Error

type Error = Mapper[error]

type Float32

type Float32 = Mapper[float32]

type Float64

type Float64 = base.Float64

type Group added in v0.35.0

type Group[K comparable, T any] struct {
	Key   K
	Items []T
}

Group holds a grouping key and its collected items.

type Int

type Int = base.Int

type Mapper

type Mapper[T any] = base.Mapper[T]

Type aliases for types defined in internal/base. All methods defined on the base types are available through these aliases.

func Compact added in v0.28.0

func Compact[T comparable](ts []T) Mapper[T]

Compact removes zero-value elements from ts.

func From

func From[T any](ts []T) Mapper[T]

From creates a Mapper for fluent operations on a slice.

func FromSet added in v0.30.0

func FromSet[T comparable](m map[T]bool) Mapper[T]

FromSet extracts the members of a set (keys where value is true) as a Mapper. Order is not guaranteed (map iteration order).

func GroupBy added in v0.30.0

func GroupBy[T any, K comparable](ts []T, fn func(T) K) Mapper[Group[K, T]]

GroupBy groups elements by the key returned by fn. Returns Mapper[Group[K, T]] — groups preserve first-seen key order and chain directly.

func Map added in v0.30.0

func Map[T, R any](ts []T, fn func(T) R) Mapper[R]

Map returns the result of applying fn to each member of ts. It is a standalone function because Go methods cannot introduce new type parameters — the target type R must be inferred from the function argument rather than bound on the receiver.

func MapAccum added in v0.25.0

func MapAccum[T, R, S any](ts []T, init S, fn func(S, T) (S, R)) (S, Mapper[R])

MapAccum threads state through a slice, producing both a final state and a mapped output. fn receives the accumulated state and current element, returning new state and an output value. Returns init and an empty slice if ts is empty.

func ParallelMap added in v0.23.0

func ParallelMap[T, R any](m Mapper[T], workers int, fn func(T) R) Mapper[R]

ParallelMap returns the result of applying fn to each member of m, using the specified number of worker goroutines. Order is preserved. The fn must be safe for concurrent use.

Example
package main

import (
	"fmt"
	"runtime"

	"github.com/binaryphile/fluentfp/slice"
)

func main() {
	double := func(n int) int { return n * 2 }
	result := slice.ParallelMap(slice.From([]int{1, 2, 3, 4, 5}), runtime.GOMAXPROCS(0), double)
	fmt.Println([]int(result))
}
Output:

[2 4 6 8 10]

func SortBy added in v0.23.0

func SortBy[T any, K cmp.Ordered](ts []T, fn func(T) K) Mapper[T]

SortBy returns a sorted copy of ts, ordered ascending by the key extracted via fn.

func SortByDesc added in v0.23.0

func SortByDesc[T any, K cmp.Ordered](ts []T, fn func(T) K) Mapper[T]

SortByDesc returns a sorted copy of ts, ordered descending by the key extracted via fn.

func UniqueBy added in v0.28.0

func UniqueBy[T any, K comparable](ts []T, fn func(T) K) Mapper[T]

UniqueBy returns a new slice with duplicate elements removed, where duplicates are determined by the key returned by fn. Preserves first occurrence and maintains order.

type MapperTo

type MapperTo[R, T any] = base.MapperTo[R, T]

func MapTo

func MapTo[R, T any](ts []T) MapperTo[R, T]

MapTo creates a MapperTo for filter→map chains where the cross-type map comes last. Prefer slice.Map(ts, fn) for most cross-type mapping — it infers all types and returns Mapper[R] for further chaining. Use MapTo[R] only when you need to filter or transform before the cross-type map: slice.MapTo[R](ts).KeepIf(pred).Map(fn).

type Rune

type Rune = Mapper[rune]

type String

type String = base.String

Jump to

Keyboard shortcuts

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