Documentation
¶
Overview ¶
Package hof provides function combinators for composition, partial application, independent application, concurrency control, side-effect wrapping, and call coalescing. Based on Stone's "Algorithms: A Functional Programming Approach" (pipe, sect, cross).
Index ¶
- func Bind[A, B, C any](f func(A, B) C, a A) func(B) C
- func BindR[A, B, C any](f func(A, B) C, b B) func(A) C
- func Cross[A, B, C, D any](f func(A) C, g func(B) D) func(A, B) (C, D)
- func Eq[T comparable](target T) func(T) bool
- func OnErr[T, R any](fn func(context.Context, T) (R, error), onErr func(error)) func(context.Context, T) (R, error)
- func Pipe[A, B, C any](f func(A) B, g func(B) C) func(A) C
- func Retry[T, R any](maxAttempts int, backoff Backoff, shouldRetry func(error) bool, ...) func(context.Context, T) (R, error)
- func Throttle[T, R any](n int, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)
- func ThrottleWeighted[T, R any](capacity int, cost func(T) int, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)
- type Backoff
- type DebounceOption
- type Debouncer
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Bind ¶
func Bind[A, B, C any](f func(A, B) C, a A) func(B) C
Bind fixes the first argument of a binary function: Bind(f, x)(y) = f(x, y). Panics if f is nil.
Example ¶
package main
import (
"fmt"
"github.com/binaryphile/fluentfp/hof"
)
func main() {
// Fix the first argument of a binary function.
add := func(a, b int) int { return a + b }
addFive := hof.Bind(add, 5)
fmt.Println(addFive(3))
}
Output: 8
func BindR ¶
func BindR[A, B, C any](f func(A, B) C, b B) func(A) C
BindR fixes the second argument of a binary function: BindR(f, y)(x) = f(x, y). Panics if f is nil.
Example ¶
package main
import (
"fmt"
"github.com/binaryphile/fluentfp/hof"
)
func main() {
// Fix the second argument of a binary function.
subtract := func(a, b int) int { return a - b }
subtractThree := hof.BindR(subtract, 3)
fmt.Println(subtractThree(10))
}
Output: 7
func Cross ¶
func Cross[A, B, C, D any](f func(A) C, g func(B) D) func(A, B) (C, D)
Cross applies two functions independently to two separate arguments. Cross(f, g)(a, b) = (f(a), g(b)). Panics if f or g is nil.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/binaryphile/fluentfp/hof"
)
func main() {
// Apply separate functions to separate arguments.
double := func(n int) int { return n * 2 }
toUpper := func(s string) string { return strings.ToUpper(s) }
both := hof.Cross(double, toUpper)
d, u := both(5, "hello")
fmt.Println(d, u)
}
Output: 10 HELLO
func Eq ¶
func Eq[T comparable](target T) func(T) bool
Eq returns a predicate that checks equality to target. T is inferred from target: hof.Eq(Skipped) returns func(Status) bool.
func OnErr ¶
func OnErr[T, R any](fn func(context.Context, T) (R, error), onErr func(error)) func(context.Context, T) (R, error)
OnErr wraps fn so that onErr is called with the error after fn returns a non-nil error. The returned function calls fn, checks for error, calls onErr(err) if present, then returns fn's original results unchanged.
onErr must be safe for concurrent use when the returned function is called from multiple goroutines.
Panics if fn is nil or onErr is nil.
Example ¶
package main
import (
"context"
"fmt"
"github.com/binaryphile/fluentfp/hof"
)
func main() {
var count int
// onErr increments the error counter.
onErr := func(_ error) { count++ }
// failOrDouble returns an error for negative inputs.
failOrDouble := func(_ context.Context, n int) (int, error) {
if n < 0 {
return 0, fmt.Errorf("negative")
}
return n * 2, nil
}
wrapped := hof.OnErr(failOrDouble, onErr)
r1, _ := wrapped(context.Background(), 5)
fmt.Println(r1, count)
r2, _ := wrapped(context.Background(), -1)
fmt.Println(r2, count)
}
Output: 10 0 0 1
func Pipe ¶
func Pipe[A, B, C any](f func(A) B, g func(B) C) func(A) C
Pipe composes two functions left-to-right: Pipe(f, g)(x) = g(f(x)). Panics if f or g is nil.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/binaryphile/fluentfp/hof"
)
func main() {
// Compose TrimSpace then ToLower into a single transform.
normalize := hof.Pipe(strings.TrimSpace, strings.ToLower)
fmt.Println(normalize(" Hello World "))
}
Output: hello world
Example (Chaining) ¶
package main
import (
"fmt"
"strconv"
"github.com/binaryphile/fluentfp/hof"
)
func main() {
// Multi-step composition uses intermediate variables.
double := func(n int) int { return n * 2 }
addOne := func(n int) int { return n + 1 }
toString := func(n int) string { return strconv.Itoa(n) }
doubleAddOne := hof.Pipe(double, addOne)
full := hof.Pipe(doubleAddOne, toString)
fmt.Println(full(5))
}
Output: 11
func Retry ¶
func Retry[T, R any](maxAttempts int, backoff Backoff, shouldRetry func(error) bool, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)
Retry wraps fn to retry on error up to maxAttempts total times. The first call is immediate; backoff(0) is the delay before the first retry. Returns the result and error from the last attempt.
shouldRetry controls which errors trigger a retry. When non-nil, only errors for which shouldRetry returns true are retried; non-retryable errors are returned immediately without backoff. When nil, all errors are retried.
Context cancellation is checked before each attempt and during backoff waits. Panics if maxAttempts < 1, backoff is nil, or fn is nil.
Example ¶
package main
import (
"context"
"fmt"
"github.com/binaryphile/fluentfp/hof"
)
func main() {
attempts := 0
// failThenSucceed fails twice, then succeeds.
failThenSucceed := func(_ context.Context, n int) (int, error) {
attempts++
if attempts < 3 {
return 0, fmt.Errorf("not yet")
}
return n * 2, nil
}
retried := hof.Retry(3, hof.ConstantBackoff(0), nil, failThenSucceed)
result, err := retried(context.Background(), 5)
fmt.Println(result, err)
}
Output: 10 <nil>
func Throttle ¶
func Throttle[T, R any](n int, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)
Throttle wraps fn with count-based concurrency control. At most n calls to fn execute concurrently. The returned function blocks until a slot is available, then calls fn. The returned function is safe for concurrent use from multiple goroutines. Panics if n <= 0 or fn is nil.
Example ¶
package main
import (
"context"
"fmt"
"github.com/binaryphile/fluentfp/hof"
)
func main() {
// Wrap a function so at most 3 calls run concurrently.
// doubleIt doubles the input.
doubleIt := func(_ context.Context, n int) (int, error) { return n * 2, nil }
throttled := hof.Throttle(3, doubleIt)
result, err := throttled(context.Background(), 5)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(result)
}
Output: 10
func ThrottleWeighted ¶
func ThrottleWeighted[T, R any](capacity int, cost func(T) int, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)
ThrottleWeighted wraps fn with cost-based concurrency control. The total cost of concurrently-executing calls never exceeds capacity. The returned function blocks until enough budget is available. The returned function is safe for concurrent use from multiple goroutines.
Token acquisition is serialized to prevent partial-acquire deadlock. This means a high-cost waiter blocks later callers even if capacity is available for them (head-of-line blocking).
Panics if capacity <= 0, cost is nil, or fn is nil. Per-call: panics if cost(t) <= 0 or cost(t) > capacity.
Example ¶
package main
import (
"context"
"fmt"
"github.com/binaryphile/fluentfp/hof"
)
func main() {
// Wrap a function so total cost of concurrent calls never exceeds 100.
// processItem returns the item unchanged.
processItem := func(_ context.Context, n int) (int, error) { return n, nil }
// itemCost uses the item value as its cost.
itemCost := func(n int) int { return n }
throttled := hof.ThrottleWeighted(100, itemCost, processItem)
result, err := throttled(context.Background(), 42)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(result)
}
Output: 42
Types ¶
type Backoff ¶
Backoff computes the delay before retry number n (0-indexed). Called between attempts: backoff(0) is the delay before the first retry.
func ConstantBackoff ¶
ConstantBackoff returns a Backoff that always waits delay.
func ExponentialBackoff ¶
ExponentialBackoff returns a Backoff with full jitter: random in [0, initial * 2^n). Panics if initial <= 0.
type DebounceOption ¶
type DebounceOption func(*debounceConfig)
DebounceOption configures a Debouncer.
func MaxWait ¶
func MaxWait(d time.Duration) DebounceOption
MaxWait caps the maximum delay under continuous activity. When continuous calls keep resetting the trailing timer, MaxWait guarantees execution after this duration from the first call in a burst. Zero (default) means no cap — trailing edge only, which can defer indefinitely under continuous activity. Panics if d < 0.
type Debouncer ¶
type Debouncer[T any] struct { // contains filtered or unexported fields }
Debouncer coalesces rapid calls, executing fn with the latest value after a quiet period of at least wait. At most one fn execution runs at a time; calls during execution queue the latest value for a fresh timer cycle after completion.
A single owner goroutine manages all state — no mutex contention, no stale timer callbacks. Call, Cancel, and Flush communicate via channels; the owner processes events sequentially.
Value capture: Call stores the latest T by value. No deep copy is performed. If T contains pointers, slices, or maps, the caller must not mutate their contents after Call.
Panic behavior: fn runs in a spawned goroutine. If fn panics, the owner goroutine's state is preserved via deferred completion signaling, and the panic propagates normally (typically crashing the process).
Reentrancy: Call and Cancel are safe to invoke from within fn on the same Debouncer. Flush and Close from within fn will deadlock — fn completion must signal before either can proceed.
Close must be called when the Debouncer is no longer needed to stop the owner goroutine. Use-after-Close panics. Close is idempotent. Operations concurrent with Close may block until Close completes, then panic.
func NewDebouncer ¶
func NewDebouncer[T any](wait time.Duration, fn func(T), opts ...DebounceOption) *Debouncer[T]
NewDebouncer creates a trailing-edge debouncer that executes fn with the latest value after wait elapses with no new calls. Panics if wait <= 0 or fn is nil.
func (*Debouncer[T]) Call ¶
func (d *Debouncer[T]) Call(v T)
Call schedules fn with v. If a previous call is pending, its value is replaced with v and the trailing timer resets. If fn is currently executing, v is queued for a fresh timer cycle after completion.
func (*Debouncer[T]) Cancel ¶
Cancel stops any pending execution. Returns true if pending work was canceled, false if there was nothing pending. If a Flush is blocked waiting for pending work, Cancel unblocks it and the Flush returns false.
func (*Debouncer[T]) Close ¶
func (d *Debouncer[T]) Close()
Close stops the owner goroutine. Any pending work is discarded. If fn is currently executing, Close waits for it to complete. If a Flush triggered the currently running execution, Flush returns true (the execution completes). If a Flush is waiting for pending work that Close discards, Flush returns false. Close is idempotent — subsequent calls return immediately. After Close, Call, Cancel, and Flush will panic. Operations concurrent with Close may block until Close completes.
Close must not be called from within fn on the same Debouncer — this will deadlock because fn completion must signal before Close can proceed.
func (*Debouncer[T]) Flush ¶
Flush executes pending work immediately. Returns true if fn was executed as a result of this call, false if there was nothing pending.
When fn is already running with pending work queued, Flush blocks until the current fn completes and the pending work executes. New Calls that arrive during a flushed execution do not extend the Flush — they are scheduled normally via timer after Flush returns.
Only one Flush waiter is supported at a time. If a Flush is already waiting, subsequent Flush calls return false immediately.
Flush must not be called from within fn on the same Debouncer — this will deadlock because fn completion must signal before Flush can proceed.