diff --git a/benchmarks/bundle-size/fp/flow.spec.ts b/benchmarks/bundle-size/fp/flow.spec.ts new file mode 100644 index 0000000000..fb7d8b9817 --- /dev/null +++ b/benchmarks/bundle-size/fp/flow.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { getBundleSizeFromScript } from '../utils/getBundleSize'; + +describe('fp/flow bundle size', () => { + it('es-toolkit/fp', async () => { + expect( + await getBundleSizeFromScript('import { flow } from "es-toolkit/fp"; console.log(flow)') + ).toMatchInlineSnapshot(`969`); + }); + + it('lodash/fp/flow', async () => { + expect(await getBundleSizeFromScript('import flow from "lodash/fp/flow"; console.log(flow)')).toMatchInlineSnapshot( + `51819` + ); + }); +}); diff --git a/benchmarks/performance/fp/flow.bench.ts b/benchmarks/performance/fp/flow.bench.ts new file mode 100644 index 0000000000..a9171830a5 --- /dev/null +++ b/benchmarks/performance/fp/flow.bench.ts @@ -0,0 +1,53 @@ +import { bench, describe } from 'vitest'; +import { filter as filterToolkit, flow as flowToolkit, map as mapToolkit, take as takeToolkit } from 'es-toolkit/fp'; +import lodashFp from 'lodash/fp'; + +const array = Array.from({ length: 10000 }, (_, i) => i); +const { filter: filterLodash, flow: flowLodash, map: mapLodash, take: takeLodash } = lodashFp; + +describe('flow/scalar composition', () => { + const add = (x: number, y: number) => x + y; + const square = (n: number) => n * n; + + bench('es-toolkit/fp', () => { + const combined = flowToolkit(add, square); + combined(1, 2); + }); + + bench('lodash/fp', () => { + const combined = flowLodash(add, square); + combined(1, 2); + }); +}); + +describe('flow/map', () => { + bench('es-toolkit/fp', () => { + const combined = flowToolkit(mapToolkit((x: number) => x * 2)); + combined(array); + }); + + bench('lodash/fp', () => { + const combined = flowLodash(mapLodash((x: number) => x * 2)); + combined(array); + }); +}); + +describe('flow/map + filter + take(5) (lazy short-circuit)', () => { + bench('es-toolkit/fp', () => { + const combined = flowToolkit( + mapToolkit((x: number) => x * 2), + filterToolkit(x => x % 3 === 0), + takeToolkit(5) + ); + combined(array); + }); + + bench('lodash/fp', () => { + const combined = flowLodash( + mapLodash((x: number) => x * 2), + filterLodash((x: number) => x % 3 === 0), + takeLodash(5) + ); + combined(array); + }); +}); diff --git a/docs/fp/reference/flow.md b/docs/fp/reference/flow.md new file mode 100644 index 0000000000..b837ea0a76 --- /dev/null +++ b/docs/fp/reference/flow.md @@ -0,0 +1,74 @@ +# flow (Functional Programming) + +Performs left-to-right function composition, returning a reusable function instead of running immediately. + +```typescript +const fn = flow(...functions); +const result = fn(...args); +``` + +::: info + +`flow` is the deferred sibling of [`pipe`](./pipe.md). Use `flow` when you want to build a reusable pipeline once and call it later with different data; use `pipe` when you already have the value and want the result right away. + +::: + +## Usage + +`flow` takes a sequence of functions and composes them left-to-right into a single function. The first function may take any number of arguments; every later function is unary and receives the result of the previous one. + +The key difference from `pipe`: + +- `pipe(value, ...functions)` threads a concrete `value` through the functions **immediately** and returns the result. +- `flow(...functions)` returns a **new function** that applies the same composition whenever you call it, so you can reuse it. + +```typescript +import { flow } from 'es-toolkit/fp'; + +const addThenSquare = flow( + (x: number, y: number) => x + y, + n => n * n +); + +addThenSquare(1, 2); // => 9 +addThenSquare(2, 3); // => 25 +``` + +Internally `flow` delegates to `pipe`, so any `es-toolkit/fp` function (or any unary function) slots in the same way, and lazy-capable functions are fused identically. + +```typescript +import { filter, flow, map, take } from 'es-toolkit/fp'; + +const firstTwoEvenSquares = flow( + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) +); + +firstTwoEvenSquares([1, 2, 3, 4, 5, 6, 7, 8]); // => [4, 16] +``` + +### Lazy evaluation + +Because `flow` runs through `pipe`, consecutive lazy-capable functions (`map`, `filter`, `take`, …) are fused and processed element-by-element instead of building an intermediate array after every step. A trailing `take` can terminate the walk early, so the earlier functions never run on the rest of the input — exactly as in `pipe`. + +```typescript +import { filter, flow, map, take } from 'es-toolkit/fp'; + +// Only the first two even squares are computed; the rest of the array is never visited. +const pipeline = flow( + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) +); + +pipeline([1, 2, 3, 4, 5, 6, 7, 8]); // => [4, 16] +``` + +#### Parameters + +- `functions` (`Array<(...args: any[]) => any>`): The functions to compose, from left to right. The first may take any number of arguments; the rest are unary, each receiving the previous function's output. + +#### Returns + +(`(...args: any[]) => unknown`): A new function that applies every function in sequence. It accepts the same parameters as the first function and returns the result of the last. The public overloads infer the precise types from the chain. diff --git a/docs/ja/fp/reference/flow.md b/docs/ja/fp/reference/flow.md new file mode 100644 index 0000000000..4f34ed70a0 --- /dev/null +++ b/docs/ja/fp/reference/flow.md @@ -0,0 +1,74 @@ +# flow (関数型プログラミング) + +左から右へ関数を合成しますが、すぐには実行せず、再利用できる関数を返します。 + +```typescript +const fn = flow(...functions); +const result = fn(...args); +``` + +::: info + +`flow` は [`pipe`](./pipe.md) の遅延実行版です。パイプラインを一度作っておき、あとで別のデータで何度も呼び出したいときは `flow` を、すでに値があってすぐに結果が欲しいときは `pipe` を使用してください。 + +::: + +## 使用法 + +`flow` は一連の関数を受け取り、左から右へ 1 つの関数に合成します。最初の関数は任意の数の引数を受け取れますが、それ以降の関数はすべて単項で、直前の関数の結果を受け取ります。 + +`pipe` との主な違いは次のとおりです。 + +- `pipe(value, ...functions)` は具体的な `value` を関数群に**すぐに**通して結果を返します。 +- `flow(...functions)` は、呼び出すたびに同じ合成を適用する**新しい関数**を返すため、再利用できます。 + +```typescript +import { flow } from 'es-toolkit/fp'; + +const addThenSquare = flow( + (x: number, y: number) => x + y, + n => n * n +); + +addThenSquare(1, 2); // => 9 +addThenSquare(2, 3); // => 25 +``` + +`flow` は内部で `pipe` に委譲するため、任意の `es-toolkit/fp` 関数(または単項関数)を同じように組み込めますし、遅延評価に対応した関数も同じように融合されます。 + +```typescript +import { filter, flow, map, take } from 'es-toolkit/fp'; + +const firstTwoEvenSquares = flow( + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) +); + +firstTwoEvenSquares([1, 2, 3, 4, 5, 6, 7, 8]); // => [4, 16] +``` + +### 遅延評価 + +`flow` は `pipe` を通して実行されるため、遅延評価に対応した関数(`map`、`filter`、`take` など)が連続して並ぶと、それらを融合し、各ステップごとに中間配列を作るのではなく入力を要素ごとに処理します。末尾の `take` は走査を早期に終了できるため、それより前の関数は残りの入力に対して実行されません。`pipe` とまったく同じ動作です。 + +```typescript +import { filter, flow, map, take } from 'es-toolkit/fp'; + +// 最初の 2 つの偶数の平方だけが計算されます。配列の残りは処理されません。 +const pipeline = flow( + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) +); + +pipeline([1, 2, 3, 4, 5, 6, 7, 8]); // => [4, 16] +``` + +#### パラメータ + +- `functions` (`Array<(...args: any[]) => any>`): 左から右へ合成する関数です。最初の関数は任意の数の引数を受け取れますが、残りはすべて単項で、直前の関数の出力を受け取ります。 + +#### 戻り値 + +(`(...args: any[]) => unknown`): すべての関数を順番に適用する新しい関数です。最初の関数と同じパラメータを受け取り、最後の関数の結果を返します。公開されているオーバーロードは、チェーンから正確な型を推論します。 diff --git a/docs/ko/fp/reference/flow.md b/docs/ko/fp/reference/flow.md new file mode 100644 index 0000000000..93f698970e --- /dev/null +++ b/docs/ko/fp/reference/flow.md @@ -0,0 +1,74 @@ +# flow (함수형 프로그래밍) + +왼쪽에서 오른쪽으로 함수를 합성하되, 바로 실행하지 않고 재사용할 수 있는 함수를 반환해요. + +```typescript +const fn = flow(...functions); +const result = fn(...args); +``` + +::: info + +`flow`는 [`pipe`](./pipe.md)의 지연 실행 버전이에요. 파이프라인을 한 번 만들어 두고 나중에 다른 데이터로 여러 번 호출하고 싶을 때는 `flow`를, 이미 값을 가지고 있어서 결과를 바로 얻고 싶을 때는 `pipe`를 사용하세요. + +::: + +## 사용법 + +`flow`는 여러 함수를 받아서 왼쪽에서 오른쪽으로 하나의 함수로 합성해요. 첫 번째 함수는 인자를 몇 개든 받을 수 있고, 그 뒤의 함수들은 모두 인자를 하나 받아서 이전 함수의 결과를 넘겨받아요. + +`pipe`와의 핵심 차이는 이래요. + +- `pipe(value, ...functions)`는 구체적인 `value`를 함수들에 **즉시** 흘려보내고 결과를 반환해요. +- `flow(...functions)`는 같은 합성을 호출할 때마다 적용하는 **새로운 함수**를 반환해서, 재사용할 수 있어요. + +```typescript +import { flow } from 'es-toolkit/fp'; + +const addThenSquare = flow( + (x: number, y: number) => x + y, + n => n * n +); + +addThenSquare(1, 2); // => 9 +addThenSquare(2, 3); // => 25 +``` + +`flow`는 내부적으로 `pipe`에 위임하기 때문에, 모든 `es-toolkit/fp` 함수(또는 인자를 하나 받는 함수)를 똑같이 끼워 넣을 수 있고, 지연 평가가 가능한 함수들도 동일하게 합쳐져요. + +```typescript +import { filter, flow, map, take } from 'es-toolkit/fp'; + +const firstTwoEvenSquares = flow( + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) +); + +firstTwoEvenSquares([1, 2, 3, 4, 5, 6, 7, 8]); // => [4, 16] +``` + +### 지연 평가 + +`flow`는 `pipe`를 통해 실행되기 때문에, 지연 평가가 가능한 함수들(`map`, `filter`, `take`, …)이 연달아 나오면 이들을 합쳐서 매 단계마다 중간 배열을 만드는 대신 입력을 요소 하나씩 처리해요. 마지막에 오는 `take`가 순회를 일찍 끝낼 수 있어서, 앞쪽 함수들은 나머지 입력에 대해서는 아예 실행되지 않아요. `pipe`와 똑같이 동작해요. + +```typescript +import { filter, flow, map, take } from 'es-toolkit/fp'; + +// 처음 두 개의 짝수 제곱만 계산해요. 배열의 나머지는 한 번도 방문하지 않아요. +const pipeline = flow( + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) +); + +pipeline([1, 2, 3, 4, 5, 6, 7, 8]); // => [4, 16] +``` + +#### 파라미터 + +- `functions` (`Array<(...args: any[]) => any>`): 왼쪽에서 오른쪽으로 합성할 함수들이에요. 첫 번째 함수는 인자를 몇 개든 받을 수 있고, 나머지는 모두 인자를 하나 받아서 이전 함수의 출력을 넘겨받아요. + +#### 반환 값 + +(`(...args: any[]) => unknown`): 모든 함수를 순서대로 적용하는 새로운 함수예요. 첫 번째 함수와 같은 파라미터를 받아서 마지막 함수의 결과를 반환해요. 공개된 오버로드는 체인에서 정확한 타입을 추론해요. diff --git a/docs/zh_hans/fp/reference/flow.md b/docs/zh_hans/fp/reference/flow.md new file mode 100644 index 0000000000..3d4d025542 --- /dev/null +++ b/docs/zh_hans/fp/reference/flow.md @@ -0,0 +1,74 @@ +# flow (函数式编程) + +执行从左到右的函数组合,但不会立即运行,而是返回一个可复用的函数。 + +```typescript +const fn = flow(...functions); +const result = fn(...args); +``` + +::: info + +`flow` 是 [`pipe`](./pipe.md) 的延迟执行版本。当你想先构建一条管道,之后再用不同的数据多次调用它时,使用 `flow`;当你已经有了值并想立即得到结果时,使用 `pipe`。 + +::: + +## 用法 + +`flow` 接收一系列函数,并将它们从左到右组合成一个函数。第一个函数可以接收任意数量的参数,其后的每个函数都是一元的,接收上一个函数的结果。 + +它与 `pipe` 的主要区别在于: + +- `pipe(value, ...functions)` 会**立即**让具体的 `value` 穿过这些函数并返回结果。 +- `flow(...functions)` 返回一个**新函数**,每次调用时都会应用同样的组合,因此可以复用。 + +```typescript +import { flow } from 'es-toolkit/fp'; + +const addThenSquare = flow( + (x: number, y: number) => x + y, + n => n * n +); + +addThenSquare(1, 2); // => 9 +addThenSquare(2, 3); // => 25 +``` + +`flow` 在内部委托给 `pipe`,因此任何 `es-toolkit/fp` 函数(或任意一元函数)都能以同样的方式嵌入,支持惰性求值的函数也会以同样的方式被融合。 + +```typescript +import { filter, flow, map, take } from 'es-toolkit/fp'; + +const firstTwoEvenSquares = flow( + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) +); + +firstTwoEvenSquares([1, 2, 3, 4, 5, 6, 7, 8]); // => [4, 16] +``` + +### 惰性求值 + +由于 `flow` 通过 `pipe` 执行,当多个支持惰性求值的函数(`map`、`filter`、`take`……)连续出现时,它们会被融合在一起,逐个元素地处理输入,而不是在每一步之后都构建一个中间数组。位于末尾的 `take` 可以提前结束遍历,这样前面的函数就不会再对剩余的输入运行——与 `pipe` 完全一致。 + +```typescript +import { filter, flow, map, take } from 'es-toolkit/fp'; + +// 只会计算前两个为偶数的平方;数组的其余部分都不会被访问。 +const pipeline = flow( + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) +); + +pipeline([1, 2, 3, 4, 5, 6, 7, 8]); // => [4, 16] +``` + +#### 参数 + +- `functions` (`Array<(...args: any[]) => any>`): 从左到右组合的函数。第一个函数可以接收任意数量的参数,其余函数都是一元的,接收上一个函数的输出。 + +#### 返回值 + +(`(...args: any[]) => unknown`): 一个依次应用所有函数的新函数。它接收与第一个函数相同的参数,并返回最后一个函数的结果。公开的重载会从整条链中推断出精确的类型。 diff --git a/src/fp/flow.spec.ts b/src/fp/flow.spec.ts new file mode 100644 index 0000000000..53a5c77942 --- /dev/null +++ b/src/fp/flow.spec.ts @@ -0,0 +1,106 @@ +import { describe, expect, expectTypeOf, it, vi } from 'vitest'; +import { filter } from './array/filter.ts'; +import { map } from './array/map.ts'; +import { take } from './array/take.ts'; +import { flow } from './flow.ts'; +import { pipe } from './pipe.ts'; + +describe('flow', () => { + it('returns its single argument unchanged when no functions are given', () => { + // No public overload accepts zero functions, but the runtime is defined for + // it; cast to exercise the identity behaviour. + const identity = (flow as () => (value: number) => number)(); + expect(identity(42)).toBe(42); + }); + + it('lets the first function take multiple arguments', () => { + const combined = flow( + (x: number, y: number) => x + y, + (n: number) => n * n + ); + + expect(combined(1, 2)).toBe(9); + }); + + it('passes the first function its full argument list', () => { + const combined = flow( + (a: number, b: number, c: number) => a + b + c, + (n: number) => n * 2 + ); + + expect(combined(1, 2, 3)).toBe(12); + }); + + it('composes several unary functions left-to-right', () => { + const combined = flow( + (x: number) => x + 1, + x => x * 3, + x => `value: ${x}` + ); + + expect(combined(1)).toBe('value: 6'); + }); + + it('works with arbitrary unary functions', () => { + const normalize = flow( + (s: string) => s.trim(), + s => s.toLowerCase() + ); + + expect(normalize(' Hello ')).toBe('hello'); + }); + + it('produces the same result as pipe for a lazy operator chain', () => { + const data = [1, 2, 3, 4, 5, 6, 7, 8]; + + const pipeline = flow( + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) + ); + + expect(pipeline(data)).toEqual( + pipe( + data, + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) + ) + ); + expect(pipeline(data)).toEqual([4, 16]); + }); + + it('keeps a leading lazy operator inside the fused, short-circuiting run', () => { + const mapSpy = vi.fn((x: number) => x * x); + + const pipeline = flow( + map(mapSpy), + filter(x => x % 2 === 0), + take(2) + ); + + expect(pipeline([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([4, 16]); + // Fused with `take(2)`, the walk stops after the fourth element rather than + // mapping the whole array — exactly as `pipe` would. + expect(mapSpy).toHaveBeenCalledTimes(4); + }); + + it('returns a reusable function that can be called many times', () => { + const double = flow((x: number) => x * 2); + + expect(double(1)).toBe(2); + expect(double(2)).toBe(4); + expect(double(3)).toBe(6); + }); + + it('infers the first function parameters and the final return type', () => { + const combined = flow( + (x: number, y: string) => x + y.length, + (n: number) => n > 0, + (b: boolean): string => (b ? 'yes' : 'no') + ); + + expectTypeOf(combined).parameters.toEqualTypeOf<[number, string]>(); + expectTypeOf(combined).returns.toEqualTypeOf(); + }); +}); diff --git a/src/fp/flow.ts b/src/fp/flow.ts new file mode 100644 index 0000000000..e697a2a5f1 --- /dev/null +++ b/src/fp/flow.ts @@ -0,0 +1,402 @@ +import { pipe } from './pipe.ts'; + +/** + * Creates a reusable function from a single function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @returns A function with the same parameters as `fn1` that returns its result. + */ +export function flow(fn1: (...args: A) => R1): (...args: A) => R1; +/** + * Composes `fn1` and `fn2` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn2`. + */ +export function flow(fn1: (...args: A) => R1, fn2: (input: R1) => R2): (...args: A) => R2; +/** + * Composes `fn1` through `fn3` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn3`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3 +): (...args: A) => R3; +/** + * Composes `fn1` through `fn4` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn4`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4 +): (...args: A) => R4; +/** + * Composes `fn1` through `fn5` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn5`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5 +): (...args: A) => R5; +/** + * Composes `fn1` through `fn6` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @param fn6 - Applied to the result of `fn5`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn6`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5, + fn6: (input: R5) => R6 +): (...args: A) => R6; +/** + * Composes `fn1` through `fn7` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @param fn6 - Applied to the result of `fn5`. + * @param fn7 - Applied to the result of `fn6`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn7`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5, + fn6: (input: R5) => R6, + fn7: (input: R6) => R7 +): (...args: A) => R7; +/** + * Composes `fn1` through `fn8` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @param fn6 - Applied to the result of `fn5`. + * @param fn7 - Applied to the result of `fn6`. + * @param fn8 - Applied to the result of `fn7`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn8`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5, + fn6: (input: R5) => R6, + fn7: (input: R6) => R7, + fn8: (input: R7) => R8 +): (...args: A) => R8; +/** + * Composes `fn1` through `fn9` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @param fn6 - Applied to the result of `fn5`. + * @param fn7 - Applied to the result of `fn6`. + * @param fn8 - Applied to the result of `fn7`. + * @param fn9 - Applied to the result of `fn8`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn9`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5, + fn6: (input: R5) => R6, + fn7: (input: R6) => R7, + fn8: (input: R7) => R8, + fn9: (input: R8) => R9 +): (...args: A) => R9; +/** + * Composes `fn1` through `fn10` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @param fn6 - Applied to the result of `fn5`. + * @param fn7 - Applied to the result of `fn6`. + * @param fn8 - Applied to the result of `fn7`. + * @param fn9 - Applied to the result of `fn8`. + * @param fn10 - Applied to the result of `fn9`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn10`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5, + fn6: (input: R5) => R6, + fn7: (input: R6) => R7, + fn8: (input: R7) => R8, + fn9: (input: R8) => R9, + fn10: (input: R9) => R10 +): (...args: A) => R10; +/** + * Composes `fn1` through `fn11` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @param fn6 - Applied to the result of `fn5`. + * @param fn7 - Applied to the result of `fn6`. + * @param fn8 - Applied to the result of `fn7`. + * @param fn9 - Applied to the result of `fn8`. + * @param fn10 - Applied to the result of `fn9`. + * @param fn11 - Applied to the result of `fn10`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn11`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5, + fn6: (input: R5) => R6, + fn7: (input: R6) => R7, + fn8: (input: R7) => R8, + fn9: (input: R8) => R9, + fn10: (input: R9) => R10, + fn11: (input: R10) => R11 +): (...args: A) => R11; +/** + * Composes `fn1` through `fn12` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @param fn6 - Applied to the result of `fn5`. + * @param fn7 - Applied to the result of `fn6`. + * @param fn8 - Applied to the result of `fn7`. + * @param fn9 - Applied to the result of `fn8`. + * @param fn10 - Applied to the result of `fn9`. + * @param fn11 - Applied to the result of `fn10`. + * @param fn12 - Applied to the result of `fn11`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn12`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5, + fn6: (input: R5) => R6, + fn7: (input: R6) => R7, + fn8: (input: R7) => R8, + fn9: (input: R8) => R9, + fn10: (input: R9) => R10, + fn11: (input: R10) => R11, + fn12: (input: R11) => R12 +): (...args: A) => R12; +/** + * Composes `fn1` through `fn13` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @param fn6 - Applied to the result of `fn5`. + * @param fn7 - Applied to the result of `fn6`. + * @param fn8 - Applied to the result of `fn7`. + * @param fn9 - Applied to the result of `fn8`. + * @param fn10 - Applied to the result of `fn9`. + * @param fn11 - Applied to the result of `fn10`. + * @param fn12 - Applied to the result of `fn11`. + * @param fn13 - Applied to the result of `fn12`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn13`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5, + fn6: (input: R5) => R6, + fn7: (input: R6) => R7, + fn8: (input: R7) => R8, + fn9: (input: R8) => R9, + fn10: (input: R9) => R10, + fn11: (input: R10) => R11, + fn12: (input: R11) => R12, + fn13: (input: R12) => R13 +): (...args: A) => R13; +/** + * Composes `fn1` through `fn14` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @param fn6 - Applied to the result of `fn5`. + * @param fn7 - Applied to the result of `fn6`. + * @param fn8 - Applied to the result of `fn7`. + * @param fn9 - Applied to the result of `fn8`. + * @param fn10 - Applied to the result of `fn9`. + * @param fn11 - Applied to the result of `fn10`. + * @param fn12 - Applied to the result of `fn11`. + * @param fn13 - Applied to the result of `fn12`. + * @param fn14 - Applied to the result of `fn13`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn14`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5, + fn6: (input: R5) => R6, + fn7: (input: R6) => R7, + fn8: (input: R7) => R8, + fn9: (input: R8) => R9, + fn10: (input: R9) => R10, + fn11: (input: R10) => R11, + fn12: (input: R11) => R12, + fn13: (input: R12) => R13, + fn14: (input: R13) => R14 +): (...args: A) => R14; +/** + * Composes `fn1` through `fn15` left-to-right into a single reusable function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the result of `fn1`. + * @param fn3 - Applied to the result of `fn2`. + * @param fn4 - Applied to the result of `fn3`. + * @param fn5 - Applied to the result of `fn4`. + * @param fn6 - Applied to the result of `fn5`. + * @param fn7 - Applied to the result of `fn6`. + * @param fn8 - Applied to the result of `fn7`. + * @param fn9 - Applied to the result of `fn8`. + * @param fn10 - Applied to the result of `fn9`. + * @param fn11 - Applied to the result of `fn10`. + * @param fn12 - Applied to the result of `fn11`. + * @param fn13 - Applied to the result of `fn12`. + * @param fn14 - Applied to the result of `fn13`. + * @param fn15 - Applied to the result of `fn14`. + * @returns A function with the same parameters as `fn1` that returns the result of `fn15`. + */ +export function flow( + fn1: (...args: A) => R1, + fn2: (input: R1) => R2, + fn3: (input: R2) => R3, + fn4: (input: R3) => R4, + fn5: (input: R4) => R5, + fn6: (input: R5) => R6, + fn7: (input: R6) => R7, + fn8: (input: R7) => R8, + fn9: (input: R8) => R9, + fn10: (input: R9) => R10, + fn11: (input: R10) => R11, + fn12: (input: R11) => R12, + fn13: (input: R12) => R13, + fn14: (input: R13) => R14, + fn15: (input: R14) => R15 +): (...args: A) => R15; +/** + * Performs left-to-right function composition, returning a **reusable function** + * instead of running immediately. The first function may take any number of + * arguments; every later function is unary and receives the previous result. + * + * `flow` is the deferred sibling of {@link pipe}: where `pipe(value, ...fns)` + * threads a concrete value through the functions right away, `flow(...fns)` + * builds a function you can call later (and reuse) with the data. Internally it + * delegates to `pipe`, so lazy-capable functions (`map`, `filter`, `take`, …) + * are fused exactly the same way and benefit from early termination. + * + * @param functions - The functions to compose. The first may be variadic; the + * rest are unary, each receiving the previous function's output. + * @returns A function that, when called, applies every function left-to-right. + * + * @example + * import { flow } from 'es-toolkit/fp'; + * + * const addThenSquare = flow( + * (x: number, y: number) => x + y, + * n => n * n + * ); + * + * addThenSquare(1, 2); // => 9 + * + * @example + * import { flow, map, filter, take } from 'es-toolkit/fp'; + * + * // A reusable lazy pipeline. Only the first two even squares are computed. + * const firstTwoEvenSquares = flow( + * map((x: number) => x * x), + * filter(x => x % 2 === 0), + * take(2) + * ); + * + * firstTwoEvenSquares([1, 2, 3, 4, 5, 6, 7, 8]); // => [4, 16] + */ +export function flow(...functions: Array<(...args: any[]) => any>): (...args: any[]) => any { + // `pipe` is variadically typed through overloads; widen it here so we can + // forward an arbitrary number of functions. + const run = pipe as (value: unknown, ...fns: Array<(input: any) => any>) => unknown; + + return function (this: any, ...args: any[]): any { + if (functions.length === 0) { + return args[0]; + } + + // With a single argument the first function is just another unary step, so + // we let `pipe` apply it too — this keeps a leading lazy operator inside the + // fused run instead of forcing an eager call. + if (args.length <= 1) { + return run(args[0], ...functions); + } + + // The first function is the only one allowed to be variadic, so it must be + // invoked directly with all arguments; the rest go through `pipe`. + const [first, ...rest] = functions; + return run(first.apply(this, args), ...rest); + }; +} diff --git a/src/fp/index.spec.ts b/src/fp/index.spec.ts index 5bc54b8bcf..6c11be02cc 100644 --- a/src/fp/index.spec.ts +++ b/src/fp/index.spec.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { add, chunk, filter, flatMap, map, multiply, omit, pick, pipe, sortBy, take, uniq } from './index.ts'; +import { add, chunk, filter, flatMap, flow, map, multiply, omit, pick, pipe, sortBy, take, uniq } from './index.ts'; describe('es-toolkit/fp', () => { it('exports pipe and every v1 operator from the public entry', () => { - for (const fn of [pipe, map, filter, flatMap, take, uniq, chunk, sortBy, multiply, add, pick, omit]) { + for (const fn of [pipe, flow, map, filter, flatMap, take, uniq, chunk, sortBy, multiply, add, pick, omit]) { expect(typeof fn).toBe('function'); } }); diff --git a/src/fp/index.ts b/src/fp/index.ts index 370cd52e75..00ea0fcd06 100644 --- a/src/fp/index.ts +++ b/src/fp/index.ts @@ -1,4 +1,5 @@ export * from './array/index.ts'; +export { flow } from './flow.ts'; export * from './math/index.ts'; export * from './object/index.ts'; export { pipe } from './pipe.ts';