Skip to content

A (Array)

Every operator is curried: A.fn(...)(arr), which is what makes it drop into pipe(arr, A.fn(...)). Streaming operators such as map, filter, flatMap, take, drop, takeWhile, and dropWhile can be fused into a single loop by the separate build-time @stopcock/fp-compiler plugin. Plain pipe at runtime never fuses on its own; it just calls each step in order. Terminal folds, accessor terminals, and materializing operations have different boundaries in what the compiler can fuse.

head<A>(arr: readonly A[]): Option<A>
last<A>(arr: readonly A[]): Option<A>
first<A>(arr: readonly A[]): Option<A> // alias for head
headOrUndefined<A>(arr: readonly A[]): A | undefined
lastOrUndefined<A>(arr: readonly A[]): A | undefined
headNonEmpty<A>(arr: readonly [A, ...A[]]): A
lastNonEmpty<A>(arr: readonly [A, ...A[]]): A
tail<A>(arr: readonly A[]): A[] // all except first
init<A>(arr: readonly A[]): A[] // all except last
length<A>(arr: A[]): number
isEmpty<A>(arr: A[]): boolean

head, last, length, and isEmpty are accessor terminals: the compiler can treat them as the last step of a fused loop. Option-returning names are the default; *OrUndefined is the explicit JavaScript interop escape hatch.

map<A, B>(f: (a: A) => B): (arr: A[]) => B[]
mapWithIndex<A, B>(f: (a: A, i: number) => B): (arr: A[]) => B[]
filter<A>(pred: (a: A) => boolean): (arr: A[]) => A[]
filterWithIndex<A>(pred: (a: A, i: number) => boolean): (arr: A[]) => A[]
flatMap<A, B>(f: (a: A) => B[]): (arr: A[]) => B[]
reduce<A, B>(f: (acc: B, a: A) => B, init: B): (arr: A[]) => B
reduceRight<A, B>(f: (acc: B, a: A) => B, init: B): (arr: A[]) => B
scan<A, B>(f: (acc: B, a: A) => B, init: B): (arr: A[]) => B[]
forEach<A>(f: (a: A) => void): (arr: A[]) => void
forEachWithIndex<A>(f: (a: A, i: number) => void): (arr: A[]) => void

map, mapWithIndex, filter, filterWithIndex, and flatMap are the streaming steps the compiler can fuse into one loop. reduce is a terminal fold. forEach is a terminal side-effect traversal. scan returns an intermediate array of every running value.

sort(arr: number[]): number[]
sortBy<A>(cmp: (a: A, b: A) => number): (arr: A[]) => A[]
reverse<A>(arr: A[]): A[]

Materialization boundaries: they need the whole array before they can produce anything. sort and sortBy always run a full sort; a following take does not get a bounded top-k shortcut.

take(n: number): <A>(arr: A[]) => A[]
drop(n: number): <A>(arr: A[]) => A[]
takeWhile<A>(pred: (a: A) => boolean): (arr: A[]) => A[]
dropWhile<A>(pred: (a: A) => boolean): (arr: A[]) => A[]
chunk(n: number): <A>(arr: A[]) => A[][]
slidingWindow(n: number): <A>(arr: A[]) => A[][]
aperture(n: number): <A>(arr: A[]) => A[][]

take, drop, takeWhile, and dropWhile are streaming steps the compiler can fuse. take and takeWhile can stop the loop early.

find<A>(pred: (a: A) => boolean): (arr: readonly A[]) => Option<A>
findIndex<A>(pred: (a: A) => boolean): (arr: readonly A[]) => Option<number>
findOrUndefined<A>(pred: (a: A) => boolean): (arr: readonly A[]) => A | undefined
findIndexOrUndefined<A>(pred: (a: A) => boolean): (arr: readonly A[]) => number | undefined
every<A>(pred: (a: A) => boolean): (arr: A[]) => boolean
some<A>(pred: (a: A) => boolean): (arr: A[]) => boolean
includes<A>(value: A): (arr: A[]) => boolean

find, every, and some are terminal predicates/searches: the compiler can run them as the last step of a fused loop and bail out early.

uniq<A>(arr: A[]): A[]
uniqBy<A, B>(f: (a: A) => B): (arr: A[]) => A[]

Deduplication needs seen-value state and returns a materialized array.

intersection<A>(b: A[]): (a: A[]) => A[]
union<A>(b: A[]): (a: A[]) => A[]
difference<A>(b: A[]): (a: A[]) => A[]
symmetricDifference<A>(b: A[]): (a: A[]) => A[]
zip<A, B>(b: B[]): (a: A[]) => [A, B][]
zipWith<A, B, C>(b: B[], f: (a: A, b: B) => C): (a: A[]) => C[]
xprod<A, B>(b: B[]): (a: A[]) => [A, B][]
groupBy<A>(f: (a: A) => string): (arr: readonly A[]) => Record<string, A[]>
partition<A>(pred: (a: A) => boolean): (arr: A[]) => [A[], A[]]
intersperse<A>(sep: A): (arr: A[]) => A[]
flatten<A>(arr: A[][]): A[]
transpose<A>(arr: A[][]): A[][]

groupBy is a materialization boundary because it needs the whole grouped result.

adjust<A>(index: number, f: (a: A) => A): (arr: A[]) => A[]
update<A>(index: number, value: A): (arr: A[]) => A[]
insert<A>(index: number, value: A): (arr: A[]) => A[]
remove(index: number, count: number): <A>(arr: A[]) => A[]

All return new arrays.

range(start: number, end: number): number[]
repeat<A>(n: number): (value: A) => A[]
times(n: number): <A>(f: (i: number) => A) => A[]
unfold<A, B>(seed: B): (f: (seed: B) => [A, B] | undefined) => A[]
import { pipe } from '@stopcock/fp'
import * as A from '@stopcock/fp/array'
type Product = {
name: string
price: number
category: string
inStock: boolean
}
// the compiler can fuse filter -> map -> take into one loop;
// plain pipe just runs each step in order
const deals = pipe(
products,
A.filter((p: Product) => p.inStock && p.price < 50),
A.map((p) => ({ name: p.name, price: p.price })),
A.take(10),
)
// group + count
const byCategory = pipe(
products,
A.groupBy((p: Product) => p.category),
)
// { electronics: [...], clothing: [...] }
// rolling total
pipe(
[100, 200, 150, 300],
A.scan((acc, x) => acc + x, 0),
)
// [100, 300, 450, 750]
// remove duplicates by key
const uniqueCustomers = pipe(
orders,
A.uniqBy((o: { customerId: string }) => o.customerId),
)
// set operations
const newUsers = pipe(allUsers, A.difference(existingUsers))
const commonTags = pipe(tagsA, A.intersection(tagsB))
// batch processing
const batches = pipe(records, A.chunk(100))