Composition utilities
The Composition module is the engine room of this library. Beyond pipe and flow, it provides a collection of small, focused utilities to shape, branch, cache, and adapt functions before they enter a pipeline.
This guide acts as a comprehensive reference for these tools. If you are new to the concepts of piping and flow, start with Thinking in pipelines first.
pipe and flow
Section titled “pipe and flow”These two functions sequence transformations top-to-bottom.
pipe evaluates a starting value immediately through a sequence of steps. flow defers execution, returning a reusable function.
Conditional execution: when and unless
Section titled “Conditional execution: when and unless”Piping raw values through standard functions works well for clean, linear logic. However, real-world applications frequently require optional branches. Rather than forcing you to write noisy inline arrow functions with ternary expressions, Composition provides declarative branching combinators that slot into pipe and flow.
when executes a transformation only when a predicate evaluates to true, whereas unless executes the transformation only when the predicate evaluates to false, returning the original input value unchanged otherwise:
Reshaping objects: struct
Section titled “Reshaping objects: struct”When deriving a structured record from a single input value, mapping wrapper functions can become repetitive.
struct compiles a record of field-level transformer functions into a single transformation that maps the input into a structured object:
Asynchronous pipelines: pipeAsync and flowAsync
Section titled “Asynchronous pipelines: pipeAsync and flowAsync”pipe and flow are strictly synchronous and infallible. If a step returns a Promise, standard pipe does not await it — subsequent steps receive the promise instance itself rather than the resolved value.
When a pipeline includes asynchronous transitions, use the dedicated async runners:
pipeAsync: Evaluates a starting value or promise immediately through a sequence of synchronous or asynchronous steps, returning aPromise.flowAsync: Compiles a sequence of steps into a reusable function that returns aPromise.
Currying functions
Section titled “Currying functions”A curried function accepts its arguments one at a time. curry converts a standard two-argument function into a sequence of single-argument functions:
This is highly useful for preparing multi-argument functions to slot as steps inside pipe or flow. curry is overloaded to support functions of 2, 3, or 4 arguments:
Reversing currying
Section titled “Reversing currying”uncurry is the inverse of curry. It converts a curried, single-argument function back into a standard multi-argument format (supporting 2, 3, or 4 arguments). This is useful when passing curried library helpers to external APIs that expect standard JavaScript functions:
Flipping argument order
Section titled “Flipping argument order”flip reverses the argument order of a curried binary function. Its main purpose is to adapt data-last library functions to a data-first format for use outside of pipelines:
Side effects with tap
Section titled “Side effects with tap”tap executes a function for its side effect (such as logging or performance auditing) and returns the original value unchanged, leaving the pipeline’s behavior undisturbed:
Diagnostic taps
Section titled “Diagnostic taps”For debugging, performance auditing, and background tasks, the module provides several standalone, configuration-based side-effect helpers: tapLog, tapInspect, tapAsync, and tapTime.
Printing values with tapLog
Section titled “Printing values with tapLog”tapLog logs the piped value to the console. You can optionally supply a label to prefix the log output, a custom logger destination, or a custom string formatter (which defaults to JSON.stringify for objects):
Detailed object inspection with tapInspect
Section titled “Detailed object inspection with tapInspect”For deeply nested objects, tapLog might output truncated representations. tapInspect formats objects with infinite depth by default using Node’s util.inspect engine:
Background operations with tapAsync
Section titled “Background operations with tapAsync”To trigger asynchronous operations (like telemetry hits, audits, or database writes) without blocking the pipeline or turning it into an async promise chain, use tapAsync. It runs the promise in the background, intercepts any rejections, and routes them to onError (defaulting to console.error):
Timing steps with tapTime
Section titled “Timing steps with tapTime”tapTime measures the execution duration of a side-effect function. It supports both synchronous and asynchronous operations (for promises, the duration is tracked until the promise resolves or rejects).
The configuration object requires either a label (to log to the console automatically) or an onFinish callback (to route the typed Duration elsewhere), which are mutually exclusive to avoid conflicts:
Function primitives
Section titled “Function primitives”identity returns its input argument exactly as received. It is commonly used as a fallback callback where a no-op transformation is required:
constant returns a function that always yields the same fixed value, ignoring whatever input is supplied to it:
once ensures a function is only executed on its first invocation, caching and returning that original result for all subsequent calls:
tuple and untuple adapt functions between positional multi-argument signatures and single-tuple argument signatures. Use tuple when a pipeline step yields a [A, B] pair (such as Arr.zip or juxt) that you want to pass into a standard multi-argument function, and untuple when passing a tuple-accepting function to a callback API that supplies separate arguments:
Predicate composition
Section titled “Predicate composition”To negate, combine, or project boolean checks, use the Predicate module. It provides Predicate.not, Predicate.and, Predicate.or, Predicate.using, Predicate.all, and Predicate.any. For full details, see the Predicate guide.
Caching results
Section titled “Caching results”memoize wraps a function, caching the results of repeated calls using the input argument as the cache key:
For complex object keys, you can supply a custom key-generator function to evaluate equivalence structurally, or specify a maxSize parameter to constrain memory usage via least-recently-used (LRU) eviction:
memoizeWeak performs the same caching behavior but stores entries using a WeakMap, ensuring that cache entries are automatically garbage-collected when the key object is no longer referenced in memory:
Fan out and combine
Section titled “Fan out and combine”converge takes a combining function and an array of transformers. It applies a single input value to each transformer independently, passing the collected results to the combiner:
Parallel results with juxt
Section titled “Parallel results with juxt”juxt is the simpler sibling of converge. It applies a single input to an array of transformer functions and returns all outcomes in a typed tuple:
Comparing by projection
Section titled “Comparing by projection”on takes a binary comparator and a projection function. It projects both input arguments before comparing them, removing the need to repeat key lookups in sorting and equality functions:
Problems it solves
Section titled “Problems it solves”- Eliminating deeply nested calls and intermediate variables (
pipe,flow,pipeAsync,flowAsync): Sequencing synchronous and asynchronous operations top-to-bottom in execution order without temporary local variables (const step1 = ...; const step2 = ...) or inside-out nested function calls. - Declarative inline branching (
when,unless): Applying conditional transformations inside a pipeline (such as applying promotional discounts or skipping tax calculations for exempt accounts) without breaking the linear flow with manual ternary wrappers. - Deriving and combining parallel projections (
struct,converge,juxt): Computing multiple independent values from a single input — whether assembling a summary object (struct), feeding parallel calculations into a combiner (converge), or collecting tuple results (juxt)—without referencing the input variable repeatedly. - Non-intrusive pipeline observability (
tap,tapLog,tapInspect,tapAsync,tapTime): Logging values, inspecting deep payloads, firing background telemetry, and measuring step execution durations mid-pipeline without altering return types or disrupting data flow. - Adapting function signatures across boundaries (
curry,uncurry,flip,tuple,untuple,on): Bridging multi-argument callbacks, tuple pairs, and third-party APIs with data-last pipeline steps without repetitive wrapper functions. - Bounding redundant computations (
once,memoize,memoizeWeak): Guaranteeing one-time initialization for resources (once) and caching expensive pure calculations by primitive key (memoizewith optional LRUmaxSize) or object reference (memoizeWeak).