Composition utilities
The Composition module is the backbone of every pipeline in pipelined. It provides pipe and flow
for sequencing steps, and a set of surrounding utilities for shaping functions before they enter a
pipeline: currying, flipping argument order, memoizing expensive steps, applying a value to several
functions at once, and more.
This guide covers all of them. If you are new to pipe and flow, start with
Thinking in pipelines first — it covers pipeline fundamentals in
depth.
pipe, flow, and compose
Section titled “pipe, flow, and compose”These three functions express the same idea — sequencing transformations — in different ways.
pipe(value, f, g, h) applies the value immediately and returns a result. flow(f, g, h) defers
execution, returning a new function you can call or pass around:
compose is the right-to-left counterpart to flow. Where flow(f, g, h) applies f first and
h last, compose(h, g, f) runs in the reverse order — matching the traditional mathematical f ∘ g
notation:
Use compose when adapting code from libraries or codebases that use right-to-left composition
convention.
Currying: curry, curry3, curry4
Section titled “Currying: curry, curry3, curry4”A curried function takes its arguments one at a time. curry converts a two-argument function into
a curried form — call it with the first argument and get a new function waiting for the second:
This is especially useful for creating partially-applied steps for pipe and flow. curry3 and
curry4 handle three- and four-argument functions:
Reversing currying: uncurry, uncurry3, uncurry4
Section titled “Reversing currying: uncurry, uncurry3, uncurry4”uncurry is the inverse of curry. It converts a curried function back to a multi-argument form —
useful when you need to call a curried function all at once, or when passing it to an API that
expects a plain binary function:
uncurry3 and uncurry4 mirror curry3 and curry4 for three- and four-argument functions.
Flipping argument order: flip
Section titled “Flipping argument order: flip”flip reverses the argument order of a curried binary function. Its primary use is adapting
data-last functions to a data-first form when calling them outside of a pipeline:
A common pattern is using flip to adapt a data-last library function for a context where
data-first is needed — for example, applying a partially-applied transform to a specific value
rather than wrapping it in an arrow function:
Side effects without breaking the chain: tap
Section titled “Side effects without breaking the chain: tap”tap runs a function for its side effect and returns the original value unchanged. It is the
standard way to log, audit, or inspect a value mid-pipeline:
Removing tap lines leaves the pipeline’s behavior identical.
Thinking in pipelines covers tap in more detail.
Function primitives: identity, constant, and friends
Section titled “Function primitives: identity, constant, and friends”identity returns its argument unchanged. It is useful wherever a no-op callback is required —
most often in fold or match when one branch should return the value as-is:
constant creates a function that always returns the same value, ignoring its argument. It is
useful for replacing values in map, or for providing fixed fallback functions:
constTrue, constFalse, constNull, constUndefined, and constVoid are zero-argument
shortcuts for the most common constant values.
once wraps a function so it runs at most once. Every subsequent call returns the cached result
from the first invocation without calling the original function again:
Predicate combinators: not, and, or
Section titled “Predicate combinators: not, and, or”not inverts a predicate. Instead of writing (n) => !isEven(n) at every call site, you compose a
named predicate:
and and or combine two predicates with short-circuit evaluation:
These compose with each other and with not to build arbitrarily complex predicates without writing
wrapper functions.
Caching results: memoize and memoizeWeak
Section titled “Caching results: memoize and memoizeWeak”memoize wraps a function so that repeated calls with the same argument return the cached result
instead of recomputing:
By default, the argument itself is used as the cache key. For object arguments, provide a custom
keyFn so that structurally equivalent objects hit the same cache entry:
memoizeWeak works the same way but uses a WeakMap as the cache, so cached values are
garbage-collected when the key object is no longer referenced. This is useful for memoizing over
large objects:
memoizeWeak only accepts object keys — primitives are not valid WeakMap keys. Use memoize for
numbers, strings, and other primitives.
Fan out and combine: converge
Section titled “Fan out and combine: converge”converge(combiner, [fn1, fn2, ...]) returns a function that applies its single input to every
transformer independently, then passes all results to the combiner:
Because all transformers receive the same original input, converge is well-suited for building
annotated records where each field is a different view of the same source value. The returned
function is a plain unary function that slots directly into a pipeline.
Collect parallel results: juxt
Section titled “Collect parallel results: juxt”juxt([fn1, fn2, ...]) is the simpler sibling of converge. Rather than passing results to a
combiner, it collects them into a typed tuple in the same order as the function array:
When all functions return the same type, TypeScript infers a homogeneous array instead of a tuple,
making juxt a convenient fan-out for same-typed transformers:
Use juxt when you want the results as a tuple. Use converge when you need to merge them into a
different shape.
Compare by projection: on
Section titled “Compare by projection: on”on(binaryFn, projection) returns a binary function that applies projection to both of its
arguments before passing them to binaryFn. The primary use is building sort comparators without
repeating the field access:
The same pattern works for record fields — name the comparator once, use it everywhere:
on is not limited to sort comparators. Any binary function works, including equality predicates:
When to use each utility
Section titled “When to use each utility”pipe— transform a value now, reading steps top-to-bottomflow— name and reuse a transformation; compose library functions point-freecompose— right-to-left ordering matching traditionalf ∘ gnotationcurry/curry3/curry4— create partially-applied pipeline steps from multi-argument functionsuncurry/uncurry3/uncurry4— call curried functions all at once, or adapt them for callback APIs that expect plain multi-argument functionsflip— switch a function from data-last to data-first, or vice versaidentity— no-op callback that returns its argument unchangedconstant— produce a fixed value regardless of inputonce— lazy one-time initialization; singleton computationnot/and/or— compose predicates without writing wrapper functionsmemoize— skip recomputation for repeated calls with the same primitive keymemoizeWeak— same, for object keys; cache entries are garbage-collected with the keytap— observe a pipeline value for logging or side effects without changing itconverge— compute several independent derived values from one input and combine themjuxt— apply one input to several functions and collect all results as a typed tupleon— build sort comparators or equality checks over a projected property