Skip to content

Cookbook

Operations with two or more arguments support both a complete direct call and a data-last call for pipe. The recipes use both forms where each reads best.

import { pipe } from '@stopcock/fp'
import * as A from '@stopcock/fp/array'
type Product = { id: string; name: string; price: number; inStock: boolean }
const inStock = A.filter(products, (product: Product) => product.inStock)
const cards = pipe(
products,
A.filter((product: Product) => product.inStock),
A.map((product: Product) => ({
id: product.id,
label: product.name,
price: product.price,
})),
A.take(20),
)
import { compile } from '@stopcock/fp/compile'
import { explain } from '@stopcock/fp/fusion/debug'
import * as A from '@stopcock/fp/array'
const activeIds = compile(
A.filter((user: User) => user.active),
A.map((user: User) => user.id),
A.take(100),
)
activeIds(firstBatch)
activeIds(secondBatch)
console.log(
explain(
A.filter((user: User) => user.active),
A.map((user: User) => user.id),
),
)
// 'sequential' unless @stopcock/fp-compiler fused this exact call at build time

compile is a marker: uncompiled it’s identical to pipe. @stopcock/fp-compiler is what actually fuses a recognized call site into one loop at build time. Use compilePure only when every callback is pure and the extra rewrite freedom is valid for your code.

import { pipe } from '@stopcock/fp'
import * as O from '@stopcock/fp/option'
const port = pipe(
O.fromNullable(process.env.PORT),
O.map(Number),
O.filter((value: number) => Number.isSafeInteger(value) && value > 0 && value < 65_536),
O.getOrElse(() => 3000),
)
import { pipe } from '@stopcock/fp'
import * as G from '@stopcock/fp/guard'
import * as R from '@stopcock/fp/result'
type DecodeError =
{ readonly type: 'InvalidJson'; readonly cause: unknown } | { readonly type: 'ExpectedObject' }
const decodeBody = (text: string) =>
pipe(
R.tryCatch(
() => JSON.parse(text) as unknown,
(cause): DecodeError => ({ type: 'InvalidJson', cause }),
),
R.filterOrElse(G.isPlainObject, (): DecodeError => ({ type: 'ExpectedObject' })),
)
import { pipe } from '@stopcock/fp'
import * as V from '@stopcock/fp/validation'
type FieldError = {
readonly field: string
readonly message: string
}
const validateSignup = (email: string, password: string) =>
V.all([
V.fromPredicate(
email,
(value: string) => value.includes('@'),
(): FieldError => ({ field: 'email', message: 'invalid email' }),
),
pipe(
password,
V.fromPredicate(
(value: string) => value.length >= 12,
(): FieldError => ({ field: 'password', message: 'too short' }),
),
),
] as const)

Use Result.flatMap for checks that depend on earlier values. Use Validation only when every check can run independently.

import { pipe } from '@stopcock/fp'
import * as Schema from '@stopcock/fp/schema'
const userId = Schema.fromPredicate(
(value: unknown): value is string => typeof value === 'string' && value.length > 0,
() => Schema.issue('Expected a user id', ['id']),
)
const decoded = Schema.validateSync(input, userId)
const decodedInPipe = pipe(input, Schema.validateSync(userId))

The same consumer accepts a Standard Schema produced by another library.

import { pipe } from '@stopcock/fp'
import * as Iter from '@stopcock/fp/iter'
const firstTenEvenSquares = pipe(
Iter.range(1, Infinity),
Iter.filter((value) => value % 2 === 0),
Iter.map((value) => value * value),
Iter.take(10),
Iter.toArray,
)

For asynchronous sources, use AsyncIter from @stopcock/async.

import { pipe } from '@stopcock/fp'
import * as Optic from '@stopcock/fp/optic'
const city = Optic.optic<User>().prop('profile').prop('address').prop('city').value
const moved = Optic.set(city, user, 'Manchester')
const movedInPipe = pipe(user, Optic.set(city, 'Manchester'))
import { pipe } from '@stopcock/fp'
import * as A from '@stopcock/fp/array'
const regionOf = (order: Order) => order.region
const totalOf = (order: Order) => order.total
const sum = (left: number, right: number) => left + right
const revenueByRegion = A.groupMapReduce(orders, regionOf, totalOf, sum)
const revenueByRegionInPipe = pipe(orders, A.groupMapReduce(regionOf, totalOf, sum))

groupMapReduce, countBy, partitionMap, binary search, combinations, and explicit mapInto/filterInto operations live in the array subpath.

import { pipe } from '@stopcock/fp'
import * as Match from '@stopcock/fp/match'
type Payment =
| { readonly type: 'Pending'; readonly id: string }
| { readonly type: 'Paid'; readonly id: string; readonly receipt: string }
| { readonly type: 'Failed'; readonly id: string; readonly reason: string }
const paymentLabels = {
Pending: (value) => `Pending ${value.id}`,
Paid: (value) => `Receipt ${value.receipt}`,
Failed: (value) => `Failed: ${value.reason}`,
} satisfies Match.Handlers<Payment, 'type', string>
const label = (payment: Payment) =>
Match.discriminant<'type', Payment, string>('type', payment, paymentLabels)
const labelInPipe = (payment: Payment) =>
pipe(payment, Match.discriminant<'type', Payment, string>('type', paymentLabels))

The handler object must cover every tag. Use the pattern module for structural patterns once simple tagged dispatch is no longer enough.