Skip to content

Cookbook

import { pipe } from '@stopcock/fp'
import * as A from '@stopcock/fp/array'
type Product = { id: string; name: string; price: number; inStock: boolean }
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 * as V from '@stopcock/fp/validation'
type FieldError = {
readonly field: string
readonly message: string
}
const validateSignup = (email: string, password: string) =>
V.all([
V.fromPredicate(
(value: string) => value.includes('@'),
(): FieldError => ({ field: 'email', message: 'invalid email' }),
)(email),
V.fromPredicate(
(value: string) => value.length >= 12,
(): FieldError => ({ field: 'password', message: 'too short' }),
)(password),
] as const)

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

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(userId)(input)

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 = pipe(
user,
Optic.set(city, 'Manchester'),
)
import * as A from '@stopcock/fp/array'
const revenueByRegion = A.groupMapReduce(
(order: Order) => order.region,
(order: Order) => order.total,
(left: number, right: number) => left + right,
)(orders)

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

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 label = (payment: Payment) =>
Match.discriminant<'type', Payment, string>('type', {
Pending: (value) => `Pending ${value.id}`,
Paid: (value) => `Receipt ${value.receipt}`,
Failed: (value) => `Failed: ${value.reason}`,
})(payment)

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