Skip to content

fp

Terminal window
bun add @stopcock/fp

Everything you need for pipe-based FP. Arrays, strings, objects, numbers, and typed error handling. Chain array operations in pipe and they get fused into single loops at runtime.

import { pipe, A, O, R } from '@stopcock/fp'
const topScorers = pipe(
players,
A.filter(p => p.active),
A.map(p => ({ name: p.name, score: p.score })),
A.sort((a, b) => b.score - a.score),
A.take(10),
)
  • pipe(value, ...fns) passes a value through functions left to right. Fuses consecutive A.* operations into a single loop.
  • Every function works both ways: A.map(arr, fn) (data-first) and pipe(arr, A.map(fn)) (data-last).
  • Option<A> / Result<A, E> for nullable values and recoverable errors. Numeric _tag for fast branching.
NamespaceModuleWhat it does
AArraymap, filter, reduce, sort, groupBy, zip, take, flatMap + 30 more
SStringsplit, trim, replace, startsWith, slice, padStart
DDictmap, filter, get, merge, keys, values, toEntries
NNumberclamp, round, isEven, inRange, toFixed
GGuardsisString, isNumber, isArray, isNullable, isRecord
ObjObjectpick, omit, assoc, dissoc, mergeDeep, path, evolve
MMathsum, mean, median, stddev, min, max, clamp
BBooleanand, or, not, xor
LogicLogicifElse, when, unless, cond, allPass, anyPass
LensLensesview, set, over for immutable nested updates
OOptionSome, None, map, flatMap, getOrElse, fromNullable
RResultOk, Err, map, flatMap, getOrElse, tryCatch

Chain fuseable operations in pipe and they get compiled into a single loop:

// This looks like 3 passes over the array:
const result = pipe(
users, // 100,000 users
A.filter(u => u.active), // pass 1: filter
A.map(u => u.name), // pass 2: map
A.take(10), // pass 3: slice
)
// But fusion compiles it into roughly:
const result = []
for (const u of users) {
if (!u.active) continue
result.push(u.name)
if (result.length >= 10) break // early exit from take()
}
// Option: for values that might not exist
const port = pipe(
O.fromNullable(process.env.PORT),
O.map(s => parseInt(s, 10)),
O.filter(n => n > 0 && n < 65536),
O.getOrElse(() => 3000),
)
// Result: for operations that might fail
const config = pipe(
R.tryCatch(() => JSON.parse(rawConfig)),
R.map(obj => obj.settings),
R.getOrElse(() => defaults),
)

Full docs on the Option & Result page, or have a look at the individual module pages in the sidebar.