slice

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jan 19, 2026 License: MIT Imports: 0 Imported by: 0

README

For why fluentfp exists and when to use it, see the main README. For performance characteristics, see benchmarks.

fluent: simple, readable FP for slices

Key Features

  • Type-Safe: fluent avoids reflection and the any type, ensuring compile-time type safety.

  • Higher-order collection methods: fluent slices offer collection methods:

    • Map: To[Type] methods for most built-in types
    • Filter: complementary KeepIf and RemoveIf methods
    • Each: as Each
  • Fluent: higher-order methods chain since they return fluent slices. This avoids the proliferation of intermediate variables and nested code endemic to the imperative style.

  • Interoperable: fluent slices auto-convert to native slices and vice-versa, allowing them to be passed without explicit conversion to functions that accept slices. Fluent slices can be operated on by regular slice operations like indexing, slicing and ranging.

  • Concise: fluent harmonizes these features and others to keep lines of code and extra syntax to a minimum.

  • Expressive: Careful method naming, fluency and compatibility with method expressions make for beautiful code:

    titles := posts.
        KeepIf(Post.IsValid).
        ToString(Post.Title)
    

    Both IsValid and Title are methods on type Post.

  • Learnable: Because fluent slices can be used the same way as native slices, they support ranging by for loops and other imperative idioms. It is easy to mix imperative with functional style, either to learn incrementally or to use "just enough" FP and leave the rest.

Method Expressions

Method expressions are the unbound form of methods in Go. For example, given user := User{}, the following statements are automatically the same:

user.IsActive()
User.IsActive(user)

This means any no-argument method can be used as the single-argument function expected by collection methods, simply by referencing it through its type name instead of an instantiated variable.

Critical: Use value receivers for read-only methods. Method expressions only work when the receiver type matches the slice element type. slice.From(users) creates Mapper[User], so User.Method requires a value receiver:

// Works - value receiver matches slice element type
func (u User) IsActive() bool { return u.Active }
slice.From(users).KeepIf(User.IsActive)  // ✓ User.IsActive is func(User) bool

// Doesn't work - pointer receiver creates wrong signature
func (u *User) IsActive() bool { return u.Active }
slice.From(users).KeepIf(User.IsActive)  // ✗ User.IsActive is func(*User) bool, not func(User) bool

Design rule: Value receivers by default, pointer receivers only when mutating. This:

  • Enables method expressions with fluentfp
  • Eliminates nil receiver panics (the "billion dollar mistake")
  • Makes value semantics explicit

Pointer receivers are common in Go codebases, but fluentfp works with them—you just can't use method expressions with them.


Getting Started

Install fluentfp:

go get github.com/binaryphile/fluentfp

Import the package:

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

Comparison with Other Libraries

Below is a comparison of fluent with the collection operations of other popular FP libraries in Go. See ../examples/comparison/main.go for examples with nine other libraries.

Library Github Stars* Type-Safe Concise Method Exprs Fluent
binaryphile/fluentfp 1
samber/lo 17.9k
thoas/go-funk 4.8k
ahmetb/go-linq 3.5k
rjNemo/underscore 109

* as of 11/17/24


Comparison: Filtering and Mapping

Given the following slice where User has IsActive and Name methods:

users := []User{{name: "Ren", active: true}}

Plain Go:

for _, user := range users {
    if user.IsActive() {
        fmt.Println(user.Name())
    }
}

Plain Go is fine, but readability suffers from nesting. Recall that range-based for loops have multiple forms (for i, x := range, for _, x := range, for i := range, for x := range ch)—each means something different, so you must identify which form before understanding what the loop does. In the form shown here, Go also forces you to waste syntax by discarding a value.

Using fluentfp:

users is a regular slice:

slice.From(users).
    KeepIf(User.IsActive).
    ToString(User.Name).
    Each(lof.Println) // helper from fluentfp/lof

This is powerful, concise and readable. It reveals intention by relying on clarity and simplicity. It is concerned more with stating what things are doing (functional) than how the computer implements them (imperative).

Unfortunately, a rough edge of Go’s type system prevents using fmt.Println directly as an argument to Each, so we’ve substituted a function from the lof helper package. It is an annoyance that there are such cases with functions that employ variadic arguments or any, but the end result is still compelling.

Using samber/lo:

lo is the most popular library, with over 17,000 GitHub stars. It is type-safe, but not fluent, and doesn't work with method expressions:

userIsActive := func(u User, _ int) bool {
    return u.IsActive()
}
toName := func(u User, _ int) string {
    return u.Name()
}
printLn := func(s string, _ int) {
    fmt.Println(s)
}
actives := lo.Filter(users, userIsActive)
names := lo.Map(actives, toName)
lo.ForEach(names, printLn)

As you can see, lo is not concise, requiring many more lines of code. The non-fluent style requires employing intermediate variables to keep things readable. Map and Filter pass indexes to their argument, meaning that you have to wrap the IsActive and Name methods in functions that accept indexes, just to discard those indexes.


Usage

There are two slice types, Mapper[T any] and MapperTo[R, T any]. If you are only mapping to one or more of the built-in types, Mapper is the right choice.

MapperTo[R, T] is for mapping to any type, usually either your own named type or one from a library (a named type is one created with the type keyword). It is the same as Mapper but with an additional method, To. To maps to R, the return type.

Creating Fluent Slices of Built-in Types

Mapper[T] is the primary fluent slice type. You can use the slice.From function to create a fluent slice:

words := slice.From([]string{"two", "words"})

To allocate a slice of defined size, make accepts a fluent slice type:

words := make(slice.String, 0, 10)

You could have used slice.Mapper[string] rather than slice.String above, but there are several predefined type aliases for built-in types to keep the basic ones readable:

  • slice.Any
  • slice.Bool
  • slice.Byte
  • slice.Error
  • slice.Int
  • slice.Rune
  • slice.String

To create a slice mappable to an arbitrary type, use the function slice.To[R], rather than slice.From. For example, to create a slice of strings mappable to a User type:

emails := []string{"user1@example.com", "user2@example.com"}
users := slice.To[User](emails).To(UserFromEmail) // UserFromEmail not shown
Creating Fluent Slices of Arbitrary Types

Creating a fluent slice of an arbitrary type is similar:

points := slice.From([]Point{{1, 2}, {3, 4}})

But there are no predefined aliases to use with make:

points := make(slice.Mapper[Point], 0, 10)
Filtering

KeepIf and RemoveIf are the filtering methods. They take a function that returns a bool:

actives := users.KeepIf(User.IsActive)
inactives := users.RemoveIf(User.IsActive)

They come as a complementary pair to avoid the need for negation in the lower-order function, otherwise the formerly-short inactives assignment above would have to look like this:

inactives := users.KeepIf(func(u User) bool { return !u.IsActive() })
Mapping to Built-in Types

Mapper has methods for mapping to the basic built-in types. They are named To[Type]:

names := users.ToString(User.Name)

The following methods are available for mapping to built-in types. They are available on both Mapper and MapperTo:

  • ToAny
  • ToBool
  • ToByte
  • ToError
  • ToInt
  • ToRune
  • ToString

There is also a method for a special case, Convert. It maps to the same type as the original slice.

If you need a type not listed here, you can use the To method on MapperTo to map to an arbitrary type.

As mentioned, method expressions are very useful. Any method of the following form on the slice's member type can be used for mapping, i.e. one with no arguments and only one return value:

func (t MemberType) MethodName() (singleReturnValue int) {} // no arguments
Mapping to Named Types

MapperTo[R, T] is used for mapping to named types. It has the same methods as Mapper, plus a To method. Create one from a regular slice with slice.To:

drivers := slice.To[Driver](cars).To(Car.Driver)
Iterating for Side Effects

Each is the method for iterating over a slice for side effects. It takes a function that returns nothing. Again, method expressions are useful here, this time ones that don't return a value:

users.Each(User.Notify)

Patterns

These patterns demonstrate idiomatic usage drawn from production code.

Type Alias for Domain Slices

Define a type alias to enable fluent methods directly on your domain slice types:

type SliceOfUsers = slice.Mapper[User]

// Now you can declare and chain directly:
var users SliceOfUsers = fetchUsers()
actives := users.KeepIf(User.IsActive)

This avoids repeated slice.From() calls when working with the same slice type multiple times.

Method Expression Chaining

Chain method expressions for transform-then-filter pipelines:

// Normalize data, then filter invalid entries
devices := slice.From(rawDevices).
    Convert(Device.Normalize).
    KeepIf(Device.IsValid)

The method expressions Device.Normalize and Device.IsValid read as declarative descriptions of the pipeline.

Field Extraction with ToString

Extract a single field from structs into a string slice:

macs := devices.ToString(Device.GetMAC)

This replaces the common pattern:

macs := make([]string, len(devices))
for i, d := range devices {
    macs[i] = d.GetMAC()
}
Counting with KeepIf + Len

Count matching elements concisely:

activeCount := slice.From(users).
    KeepIf(User.IsActive).
    Len()

This replaces:

count := 0
for _, u := range users {
    if u.IsActive() {
        count++
    }
}

Note: This allocates an intermediate slice. For hot paths where you only need the count, a manual loop avoids allocation. See benchmarks.

Chain Formatting

Single operation - keep on one line:

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

Two or more operations - each operation on its own indented line, trailing dot:

count := slice.From(tickets).
    KeepIf(completedAfterCutoff).
    Len()

The setup (slice.From, slice.MapTo[R], etc.) doesn't count as an operation—it's scaffolding. Only the chained methods (KeepIf, ToString, Len, etc.) count. This keeps each conceptual operation visually distinct.


Standalone Functions

In addition to methods on Mapper and MapperTo, the slice package provides standalone functions for operations that return multiple values or different types.

Fold

Fold reduces a slice to a single value by applying a function to each element, processing left-to-right:

// sumFloat64 adds two float64 values.
sumFloat64 := func(acc, x float64) float64 { return acc + x }

// indexByMAC adds a device to the map keyed by its MAC address.
indexByMAC := func(m map[string]Device, d Device) map[string]Device {
    m[d.MAC] = d
    return m
}

// maxInt returns the larger of two integers.
maxInt := func(max, x int) int {
    if x > max {
        return x
    }
    return max
}

total := slice.Fold(amounts, 0.0, sumFloat64)
byMAC := slice.Fold(devices, make(map[string]Device), indexByMAC)
max := slice.Fold(values, values[0], maxInt)
Unzip2, Unzip3, Unzip4

Extract multiple fields from a slice in a single pass. More efficient than calling separate ToX methods when you need multiple fields:

// Instead of 4 iterations:
//   leadTimes := slice.From(history).ToFloat64(Record.GetLeadTime)
//   deployFreqs := slice.From(history).ToFloat64(Record.GetDeployFreq)
//   ...

// One iteration:
leadTimes, deployFreqs, mttrs, cfrs := slice.Unzip4(history,
    Record.GetLeadTime,
    Record.GetDeployFreq,
    Record.GetMTTR,
    Record.GetChangeFailRate,
)
Zip and ZipWith (pair package)

The pair package provides functions for combining two slices element-by-element. Import separately:

import "github.com/binaryphile/fluentfp/tuple/pair"

Zip creates pairs from corresponding elements:

names := []string{"Alice", "Bob", "Carol"}
scores := []int{95, 87, 92}

// Create slice of pairs
pairs := pair.Zip(names, scores)
// Result: []pair.X[string, int]{{V1: "Alice", V2: 95}, {V1: "Bob", V2: 87}, {V1: "Carol", V2: 92}}

// printPair prints a name-score pair to stdout.
printPair := func(p pair.X[string, int]) {
    fmt.Printf("%s: %d\n", p.V1, p.V2)
}
slice.From(pairs).Each(printPair)

ZipWith applies a function to corresponding elements:

// formatScore combines a name and score into a display string.
formatScore := func(name string, score int) string {
    return fmt.Sprintf("%s: %d", name, score)
}

results := pair.ZipWith(names, scores, formatScore)
// Result: []string{"Alice: 95", "Bob: 87", "Carol: 92"}

Both functions panic if slices have different lengths (fail-fast behavior).


When Loops Are Still Necessary

fluentfp handles most slice operations, but these patterns still require traditional loops:

Channel Consumption

Ranging over channels has no FP equivalent:

for result := range resultsChan {
    // process each result
}
Complex Control Flow

When you need break, continue, or early return within the loop body.

Index-Dependent Logic

When you need the index i for more than just accessing elements—e.g., comparing adjacent elements, building position-aware output, or algorithms that depend on element position.


Why Name Your Functions

Anonymous functions and higher-order functions require mental effort to parse. When using fluentfp with custom predicates or reducers, prefer named functions over inline anonymous functions. This reduces cognitive load.

The Problem with Inline Lambdas

Anonymous functions require readers to:

  1. Parse higher-order function concept (KeepIf takes a function)
  2. Parse anonymous function syntax
  3. Understand the predicate logic inline
  4. Track all this while following the chain
Named Functions Read Like English
// Hard to parse: what does this filter mean?
slice.From(tickets).KeepIf(func(t Ticket) bool { return t.CompletedTick >= cutoff }).Len()

// Reads as intent: "keep if completed after cutoff, get length"
slice.From(tickets).KeepIf(completedAfterCutoff).Len()

The second version hides the mechanics. You see intent. If you need details, you find a named function with a godoc comment. Naming also forces you to articulate intent—crystallizing your own understanding.

Documentation at the Right Boundary

Single-expression predicates go on one line:

// completedAfterCutoff returns true if ticket was completed after the cutoff tick.
completedAfterCutoff := func(t Ticket) bool { return t.CompletedTick >= cutoff }

This provides:

  • A semantic name communicating intent
  • A godoc comment explaining the predicate
  • A digestible unit of logic

For multi-statement bodies, use standard formatting:

// isActiveAdmin performs multiple validations.
isActiveAdmin := func(u User) bool {
    if u.IsDeleted() {
        return false
    }
    return u.IsActive() && u.Role == "admin"
}

This is consistent with Go's documentation practices—the comment is there when you need to dig deeper.

When to Name
Name when... Inline when...
Captures outer variables Trivial field access (func(u User) string { return u.Name })
Has domain meaning Standard idiom (t.Run, http.HandlerFunc)
Reused multiple times
Complex (multiple statements)

Documentation

Overview

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

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.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

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 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]

type Bool

type Bool = Mapper[bool]

type Byte

type Byte = Mapper[byte]

type Error

type Error = Mapper[error]

type Float32

type Float32 = Mapper[float32]

type Float64

type Float64 = Mapper[float64]

type Int

type Int = Mapper[int]

type Mapper

type Mapper[T any] []T

Mapper is a fluent slice usable anywhere a regular slice is, but provides additional fluent fp methods. Its underlying type is []T.

func From

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

func (Mapper[T]) Convert

func (ts Mapper[T]) Convert(fn func(T) T) Mapper[T]

Convert returns the result of applying fn to each member of ts.

func (Mapper[T]) Each

func (ts Mapper[T]) Each(fn func(T))

Each applies fn to each member of ts.

func (Mapper[T]) KeepIf

func (ts Mapper[T]) KeepIf(fn func(T) bool) Mapper[T]

KeepIf returns a new slice containing the members of ts for which fn returns true. It is the complement of RemoveIf.

func (Mapper[T]) Len

func (ts Mapper[T]) Len() int

Len returns the length of the slice.

func (Mapper[T]) RemoveIf

func (ts Mapper[T]) RemoveIf(fn func(T) bool) Mapper[T]

RemoveIf returns a new slice containing members for which fn returns false. It is the complement of KeepIf.

func (Mapper[T]) TakeFirst

func (ts Mapper[T]) TakeFirst(n int) Mapper[T]

TakeFirst returns the first n elements of ts.

func (Mapper[T]) ToAny

func (ts Mapper[T]) ToAny(fn func(T) any) Mapper[any]

ToAny returns the result of applying fn to each member of ts.

func (Mapper[T]) ToBool

func (ts Mapper[T]) ToBool(fn func(T) bool) Mapper[bool]

ToBool returns the result of applying fn to each member of ts.

func (Mapper[T]) ToByte

func (ts Mapper[T]) ToByte(fn func(T) byte) Mapper[byte]

ToByte returns the result of applying fn to each member of ts.

func (Mapper[T]) ToError

func (ts Mapper[T]) ToError(fn func(T) error) Mapper[error]

ToError returns the result of applying fn to each member of ts.

func (Mapper[T]) ToFloat32

func (ts Mapper[T]) ToFloat32(fn func(T) float32) Mapper[float32]

ToFloat32 returns the result of applying fn to each member of ts.

func (Mapper[T]) ToFloat64

func (ts Mapper[T]) ToFloat64(fn func(T) float64) Mapper[float64]

ToFloat64 returns the result of applying fn to each member of ts.

func (Mapper[T]) ToInt

func (ts Mapper[T]) ToInt(fn func(T) int) Mapper[int]

ToInt returns the result of applying fn to each member of ts.

func (Mapper[T]) ToRune

func (ts Mapper[T]) ToRune(fn func(T) rune) Mapper[rune]

ToRune returns the result of applying fn to each member of ts.

func (Mapper[T]) ToString

func (ts Mapper[T]) ToString(fn func(T) string) Mapper[string]

ToString returns the result of applying fn to each member of ts.

type MapperTo

type MapperTo[R, T any] []T

MapperTo 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.

func MapTo

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

func (MapperTo[R, T]) Convert

func (ts MapperTo[R, T]) Convert(fn func(T) T) MapperTo[R, T]

Convert returns the result of applying fn to each member of ts.

func (MapperTo[R, T]) Each

func (ts MapperTo[R, T]) Each(fn func(T))

Each applies fn to each member of ts.

func (MapperTo[R, T]) KeepIf

func (ts MapperTo[R, T]) KeepIf(fn func(T) bool) MapperTo[R, T]

KeepIf returns a new slice containing the members of ts for which fn returns true. It is the complement of RemoveIf.

func (MapperTo[T, R]) Len

func (ts MapperTo[T, R]) Len() int

Len returns the length of the slice.

func (MapperTo[R, T]) RemoveIf

func (ts MapperTo[R, T]) RemoveIf(fn func(T) bool) MapperTo[R, T]

RemoveIf returns a new slice containing members for which fn returns false. It is the complement of KeepIf.

func (MapperTo[R, T]) TakeFirst

func (ts MapperTo[R, T]) TakeFirst(n int) MapperTo[R, T]

TakeFirst returns the first n members of ts.

func (MapperTo[R, T]) To

func (ts MapperTo[R, T]) To(fn func(T) R) Mapper[R]

To returns the result of applying fn to each member of ts.

func (MapperTo[R, T]) ToAny

func (ts MapperTo[R, T]) ToAny(fn func(T) any) MapperTo[R, any]

ToAny returns the result of applying fn to each member of ts.

func (MapperTo[R, T]) ToBool

func (ts MapperTo[R, T]) ToBool(fn func(T) bool) MapperTo[R, bool]

ToBool returns the result of applying fn to each member of ts.

func (MapperTo[R, T]) ToByte

func (ts MapperTo[R, T]) ToByte(fn func(T) byte) MapperTo[R, byte]

ToByte returns the result of applying fn to each member of ts.

func (MapperTo[R, T]) ToError

func (ts MapperTo[R, T]) ToError(fn func(T) error) MapperTo[R, error]

ToError returns the result of applying fn to each member of ts.

func (MapperTo[R, T]) ToFloat32

func (ts MapperTo[R, T]) ToFloat32(fn func(T) float32) MapperTo[R, float32]

ToFloat32 returns the result of applying fn to each member of ts.

func (MapperTo[R, T]) ToFloat64

func (ts MapperTo[R, T]) ToFloat64(fn func(T) float64) MapperTo[R, float64]

ToFloat64 returns the result of applying fn to each member of ts.

func (MapperTo[R, T]) ToInt

func (ts MapperTo[R, T]) ToInt(fn func(T) int) MapperTo[R, int]

ToInt returns the result of applying fn to each member of ts.

func (MapperTo[R, T]) ToRune

func (ts MapperTo[R, T]) ToRune(fn func(T) rune) MapperTo[R, rune]

ToRune returns the result of applying fn to each member of ts.

func (MapperTo[R, T]) ToString

func (ts MapperTo[R, T]) ToString(fn func(T) string) MapperTo[R, string]

ToString returns the result of applying fn to each member of ts.

type Rune

type Rune = Mapper[rune]

type String

type String = Mapper[string]

Jump to

Keyboard shortcuts

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