From 308cca9c819aa6cb435880a40d03ca03ee6f026b Mon Sep 17 00:00:00 2001 From: Phryxia Date: Sat, 17 Feb 2024 14:31:14 +0900 Subject: [PATCH 1/3] refactor: extract type of function arg in `reduce` --- src/reduce.ts | 31 +++++++++++++------------------ src/types/Reducer.ts | 3 +++ 2 files changed, 16 insertions(+), 18 deletions(-) create mode 100644 src/types/Reducer.ts diff --git a/src/reduce.ts b/src/reduce.ts index 229109b8..7d3638a2 100644 --- a/src/reduce.ts +++ b/src/reduce.ts @@ -2,10 +2,11 @@ import { isAsyncIterable, isIterable } from "./_internal/utils"; import pipe1 from "./pipe1"; import type Arrow from "./types/Arrow"; import type IterableInfer from "./types/IterableInfer"; +import type { AsyncReducer, SyncReducer } from "./types/Reducer"; import type ReturnValueType from "./types/ReturnValueType"; function sync( - f: (a: Acc, b: T) => Acc, + f: SyncReducer, acc: Acc, iterable: Iterable, ): Acc { @@ -16,7 +17,7 @@ function sync( } async function async( - f: (a: Acc, b: T) => Acc, + f: SyncReducer, acc: Promise, iterable: AsyncIterable, ) { @@ -114,48 +115,42 @@ function reduce( iterable: T, ): Acc; -function reduce(f: (acc: T, value: T) => T, iterable: Iterable): T; +function reduce(f: SyncReducer, iterable: Iterable): T; -function reduce( - f: (acc: Acc, value: T) => Acc, - iterable: Iterable, -): Acc; +function reduce(f: SyncReducer, iterable: Iterable): Acc; function reduce( - f: (acc: Acc, value: T) => Acc, + f: SyncReducer, seed: Acc, iterable: Iterable, ): Acc; function reduce( - f: (acc: T, value: T) => T, + f: SyncReducer, iterable: AsyncIterable, ): Promise; function reduce( - f: (acc: Acc, value: T) => Acc | Promise, + f: AsyncReducer, seed: Acc | Promise, iterable: AsyncIterable, ): Promise; function reduce( - f: (acc: Acc, value: T) => Acc | Promise, + f: AsyncReducer, iterable: AsyncIterable, ): Promise; function reduce | AsyncIterable>( - f: ( - acc: IterableInfer, - value: IterableInfer, - ) => IterableInfer | Promise>, -): (iterable: T) => ReturnValueType>; + f: AsyncReducer, IterableInfer>, +): (iterable: T) => ReturnValueType; function reduce | AsyncIterable, Acc>( - f: (acc: Acc, value: IterableInfer) => Acc | Promise, + f: AsyncReducer>, ): (iterable: T) => ReturnValueType; function reduce | AsyncIterable, Acc>( - f: (acc: Acc, value: IterableInfer) => Acc, + f: SyncReducer>, seed?: Acc | Iterable> | AsyncIterable>, iterable?: Iterable> | AsyncIterable>, ): Acc | Promise | ((iterable: T) => ReturnValueType) { diff --git a/src/types/Reducer.ts b/src/types/Reducer.ts new file mode 100644 index 00000000..40341f84 --- /dev/null +++ b/src/types/Reducer.ts @@ -0,0 +1,3 @@ +export type SyncReducer = (acc: Acc, value: T) => Acc; + +export type AsyncReducer = (acc: Acc, value: T) => Acc | Promise; From 57e3e8c8bf2fb28f8996c1c883e1f3b83e09595f Mon Sep 17 00:00:00 2001 From: Phryxia Date: Sat, 17 Feb 2024 16:38:34 +0900 Subject: [PATCH 2/3] feat: implement `reduceLazy` (#239) --- src/Lazy/index.ts | 2 + src/Lazy/reduceLazy.ts | 63 ++++++++++++++++++++++++ test/Lazy/reduceLazy.spec.ts | 79 ++++++++++++++++++++++++++++++ type-check/Lazy/reduceLazy.test.ts | 75 ++++++++++++++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 src/Lazy/reduceLazy.ts create mode 100644 test/Lazy/reduceLazy.spec.ts create mode 100644 type-check/Lazy/reduceLazy.test.ts diff --git a/src/Lazy/index.ts b/src/Lazy/index.ts index 222cb7f0..5c31baed 100644 --- a/src/Lazy/index.ts +++ b/src/Lazy/index.ts @@ -24,6 +24,7 @@ import pipeLazy from "./pipeLazy"; import pluck from "./pluck"; import prepend from "./prepend"; import range from "./range"; +import reduceLazy from "./reduceLazy"; import reject from "./reject"; import repeat from "./repeat"; import reverse from "./reverse"; @@ -70,6 +71,7 @@ export { prepend, range, reject, + reduceLazy, repeat, reverse, scan, diff --git a/src/Lazy/reduceLazy.ts b/src/Lazy/reduceLazy.ts new file mode 100644 index 00000000..543244ec --- /dev/null +++ b/src/Lazy/reduceLazy.ts @@ -0,0 +1,63 @@ +import reduce from "../reduce"; +import type IterableInfer from "../types/IterableInfer"; +import type { AsyncReducer, SyncReducer } from "../types/Reducer"; +import type ReturnValueType from "../types/ReturnValueType"; + +type InferCarrier = T extends AsyncIterable + ? AsyncIterable + : T extends Iterable + ? T + : never; + +// DO NOT change the order of signatures prematurely. +// See `reduceLazy.test.ts` for the reason + +function reduceLazy | AsyncIterable, Acc>( + f: SyncReducer> | AsyncReducer>, + seed: Acc, +): (iterable: InferCarrier) => ReturnValueType; + +function reduceLazy | AsyncIterable>( + f: + | AsyncReducer, IterableInfer> + | SyncReducer, IterableInfer>, + seed?: IterableInfer, +): (iterable: InferCarrier) => ReturnValueType>; + +function reduceLazy( + f: SyncReducer, + seed: Acc, +): | AsyncIterable>( + iterable: C, +) => ReturnValueType; + +function reduceLazy( + f: SyncReducer, + seed?: T, +): | AsyncIterable>( + iterable: C, +) => ReturnValueType; + +function reduceLazy( + f: AsyncReducer, + seed: Acc, +): (iterable: Iterable | AsyncIterable) => Promise; + +function reduceLazy( + f: AsyncReducer, + seed?: T, +): (iterable: Iterable | AsyncIterable) => Promise; + +function reduceLazy | AsyncIterable, Acc>( + f: SyncReducer>, + seed?: Acc, +) { + if (seed === undefined) { + return (iterable: Iterable | AsyncIterable) => + reduce(f, iterable as any); + } + return (iterable: Iterable | AsyncIterable) => + reduce(f, seed, iterable as any); +} + +export default reduceLazy; diff --git a/test/Lazy/reduceLazy.spec.ts b/test/Lazy/reduceLazy.spec.ts new file mode 100644 index 00000000..34c85059 --- /dev/null +++ b/test/Lazy/reduceLazy.spec.ts @@ -0,0 +1,79 @@ +import { filter, map, pipe, range, reduceLazy, toAsync } from "../../src"; + +const addNumber = (a: number, b: number) => a + b; +const addNumberAsync = async (a: number, b: number) => a + b; + +// these tests are almost identical to `reduce.spec.ts` +describe("reduceLazy", () => { + describe("sync", () => { + it("should return initial value when the given `iterable` is empty array", () => { + const reduce = reduceLazy((a, b) => a + b, "seed"); + expect(reduce([])).toEqual("seed"); + }); + + it("should be occured error when the given `iterable` is an empty array and initial value is absent", () => { + const reduce = reduceLazy((a: number, b: number) => a + b); + expect(() => reduce([])).toThrow(); + }); + + it("should work given it is initial value", () => { + const reduce = reduceLazy(addNumber, 10); + expect(reduce(range(1, 6))).toEqual(25); + }); + + it("should use the first value as the initial value if initial value is absent", () => { + const reduce = reduceLazy(addNumber); + expect(reduce(range(1, 6))).toEqual(15); + }); + + it("should be able to be used as a curried function in the pipeline", () => { + const res = pipe( + ["1", "2", "3", "4", "5"], + map((a) => Number(a)), + filter((a) => a % 2), + reduceLazy(addNumber), + ); + expect(res).toEqual(1 + 3 + 5); + }); + }); + + describe("async", () => { + it("should reduce `iterable` by the callback", async () => { + const reduce = reduceLazy(addNumber, 10); + expect(await reduce(toAsync(range(1, 6)))).toEqual(25); + }); + + it("should use the first value as the initial value if initial value is absent", async () => { + const reduce = reduceLazy(addNumber); + expect(await reduce(toAsync(range(1, 6)))).toEqual(15); + }); + + it("should reduce `AsyncIterable` by the callback with initial value", async () => { + const reduce = reduceLazy(addNumberAsync, 10); + expect(await reduce(toAsync(range(1, 6)))).toEqual(25); + }); + + it("should reduce 'AsyncIterable' by the callback", async () => { + const reduce = reduceLazy(addNumberAsync); + expect(await reduce(toAsync(range(1, 6)))).toEqual(15); + }); + + it("should be able to be used as a curried function in the pipeline", async () => { + const res1 = await pipe( + toAsync(["1", "2", "3", "4", "5"]), + map((a) => Number(a)), + filter((a) => a % 2), + reduceLazy(addNumber), + ); + // async callback + const res2 = await pipe( + toAsync(["1", "2", "3", "4", "5"]), + map((a) => Number(a)), + filter((a) => a % 2), + reduceLazy(addNumberAsync), + ); + expect(res1).toEqual(1 + 3 + 5); + expect(res2).toEqual(1 + 3 + 5); + }); + }); +}); diff --git a/type-check/Lazy/reduceLazy.test.ts b/type-check/Lazy/reduceLazy.test.ts new file mode 100644 index 00000000..587d6cec --- /dev/null +++ b/type-check/Lazy/reduceLazy.test.ts @@ -0,0 +1,75 @@ +import { pipe, toAsync } from "../../src"; +import reduceLazy from "../../src/Lazy/reduceLazy"; +import * as Test from "../../src/types/Test"; + +const { checks, check } = Test; + +const homoSyncFn = reduceLazy((acc: number, value: number) => acc + value); +const homoSyncFnIterable = homoSyncFn([1, 2, 3]); +const homoSyncFnAsyncIterable = homoSyncFn(toAsync([1, 2, 3])); + +const homoAsyncFn = reduceLazy( + async (acc: number, value: number) => acc + value, +); +const homoAsyncFnIterable = homoAsyncFn([1, 2, 3]); +const homoAsyncFnAsyncIterable = homoAsyncFn(toAsync([1, 2, 3])); + +const heteroSyncFn = reduceLazy( + (acc: number, value: string) => acc + Number(value), + 0, +); +const heteroSyncFnIterable = heteroSyncFn(["1", "2", "3"]); +const heteroSyncFnAsyncIterable = heteroSyncFn(toAsync(["1", "2", "3"])); + +const heteroAsyncFn = reduceLazy( + async (acc: number, value: string) => acc + Number(value), + 0, +); +const heteroAsyncFnIterable = heteroAsyncFn(["1", "2", "3"]); +const heteroAsyncFnAsyncIterable = heteroAsyncFn(toAsync(["1", "2", "3"])); + +const homoPipe = pipe( + [1, 2, 3], + reduceLazy((acc, value) => acc + value), +); +const heteroPipe = pipe( + ["1", "2", "3"], + reduceLazy((acc, value) => acc + Number(value), 0), +); +const homoPipeAsync = pipe( + [1, 2, 3], + toAsync, + reduceLazy((acc, value) => acc + value), +); +const heteroPipeAsync = pipe( + ["1", "2", "3"], + toAsync, + reduceLazy((acc, value) => acc + Number(value), 0), +); +const homoPipeAsyncPromise = pipe( + [1, 2, 3], + toAsync, + reduceLazy(async (acc, value) => acc + value), +); +const heteroPipePromise = pipe( + ["1", "2", "3"], + toAsync, + reduceLazy(async (acc, value) => acc + Number(value), 0), +); + +checks([ + check(), + check, Test.Pass>(), + check, Test.Pass>(), + check, Test.Pass>(), + check(), + check, Test.Pass>(), + check, Test.Pass>(), + check, Test.Pass>(), + check(), + check(), + check, Test.Pass>(), + check, Test.Pass>(), + check, Test.Pass>(), + check, Test.Pass>(), +]); From cd3cbf6bdce216fbe00f95e2a4722efacf20319b Mon Sep 17 00:00:00 2001 From: Phryxia Date: Sat, 17 Feb 2024 17:15:46 +0900 Subject: [PATCH 3/3] docs: add jsdoc for `reduceLazy` --- src/Lazy/reduceLazy.ts | 44 ++++++++++++++++++++++++++++++++++++++++++ src/reduce.ts | 11 +++++------ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/Lazy/reduceLazy.ts b/src/Lazy/reduceLazy.ts index 543244ec..a0784e76 100644 --- a/src/Lazy/reduceLazy.ts +++ b/src/Lazy/reduceLazy.ts @@ -12,6 +12,50 @@ type InferCarrier = T extends AsyncIterable // DO NOT change the order of signatures prematurely. // See `reduceLazy.test.ts` for the reason +/** + * High order functional version of `reduce`, which behaves identical to it. + * + * @param f Reducer function `(acc, value) => acc`. It can be both synchronous and asynchronous. + * @param seed Initial value. Note that if the type of `acc` and `value` differ, `seed` must be given. + * + * @example + * Type must be provided for stand alone call. + * + * ```ts + * const reduce = reduceLazy((a: number, b: number) => a + b, 5) + * + * reduce([1, 2, 3]) // number + * reduce(toAsync([1, 2, 3])) // Promise + * ``` + * + * Fit perfectly with `pipe` + * + * ```ts + * pipe( + * [1, 2, 3, 4], + * reduceLazy((a, b) => a + b, 5) + * ); // 15 + * ``` + * + * You can use asynchronous callback + * + * ```ts + * await pipe( + * [1, 2, 3, 4], + * reduceLazy(async (a, b) => a + b, 5) + * ); // 15 + * ``` + * + * `AsyncIterable` doesn't matter. + * + * ```ts + * await pipe( + * [1, 2, 3, 4], + * toAsync, + * reduceLazy((a, b) => a + b, 5) + * ); // 15 + * ``` + */ function reduceLazy | AsyncIterable, Acc>( f: SyncReducer> | AsyncReducer>, seed: Acc, diff --git a/src/reduce.ts b/src/reduce.ts index 7d3638a2..10f76892 100644 --- a/src/reduce.ts +++ b/src/reduce.ts @@ -67,11 +67,8 @@ async function async( * ); // 26 * ``` * - * Currently, type with explicit seed form can't be inferred properly due to the limitation of typescript. - * But you can still use mostly with @ts-ignore flag. For more information please visit issue. - * - * {@link https://github.com/marpple/FxTS/issues/239 | #related issue} - * + * For backward compatibility, `reduce` can support partial lazy form. + * You may want to use `reduceLazy` to use `seed`. * * ```ts * await pipe( @@ -190,7 +187,9 @@ function reduce | AsyncIterable, Acc>( }); } - throw new TypeError("'iterable' must be type of Iterable or AsyncIterable"); + throw new TypeError( + "'iterable' must be type of Iterable or AsyncIterable. Are you looking for 'reduceLazy'?", + ); } if (isIterable(iterable)) {