Repository: marpple/FxTS
Pull request #246 (2024-02-18 11:29:48)

feat: add reduceLazy
Fixes #



Patch:
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..a0784e76
--- /dev/null
+++ b/src/Lazy/reduceLazy.ts
@@ -0,0 +1,107 @@
+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> = T extends AsyncIterable<infer R>
+  ? AsyncIterable<R>
+  : T extends Iterable<unknown>
+  ? T
+  : never;
+
+// 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<number>
+ * ```
+ *
+ * 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<T extends Iterable<unknown> | AsyncIterable<unknown>, Acc>(
+  f: SyncReducer<Acc, IterableInfer<T>> | AsyncReducer<Acc, IterableInfer<T>>,
+  seed: Acc,
+): (iterable: InferCarrier<T>) => ReturnValueType<T, Acc>;
+
+function reduceLazy<T extends Iterable<unknown> | AsyncIterable<unknown>>(
+  f:
+    | AsyncReducer<IterableInfer<T>, IterableInfer<T>>
+    | SyncReducer<IterableInfer<T>, IterableInfer<T>>,
+  seed?: IterableInfer<T>,
+): (iterable: InferCarrier<T>) => ReturnValueType<T, IterableInfer<T>>;
+
+function reduceLazy<T, Acc>(
+  f: SyncReducer<Acc, T>,
+  seed: Acc,
+): <C extends Iterable<T> | AsyncIterable<T>>(
+  iterable: C,
+) => ReturnValueType<C, Acc>;
+
+function reduceLazy<T>(
+  f: SyncReducer<T, T>,
+  seed?: T,
+): <C extends Iterable<T> | AsyncIterable<T>>(
+  iterable: C,
+) => ReturnValueType<C>;
+
+function reduceLazy<T, Acc>(
+  f: AsyncReducer<Acc, T>,
+  seed: Acc,
+): (iterable: Iterable<T> | AsyncIterable<T>) => Promise<Acc>;
+
+function reduceLazy<T>(
+  f: AsyncReducer<T, T>,
+  seed?: T,
+): (iterable: Iterable<T> | AsyncIterable<T>) => Promise<T>;
+
+function reduceLazy<T extends Iterable<unknown> | AsyncIterable<unknown>, Acc>(
+  f: SyncReducer<Acc, IterableInfer<T>>,
+  seed?: Acc,
+) {
+  if (seed === undefined) {
+    return (iterable: Iterable<unknown> | AsyncIterable<unknown>) =>
+      reduce(f, iterable as any);
+  }
+  return (iterable: Iterable<unknown> | AsyncIterable<unknown>) =>
+    reduce(f, seed, iterable as any);
+}
+
+export default reduceLazy;
diff --git a/src/reduce.ts b/src/reduce.ts
index 229109b8..10f76892 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<T, Acc>(
-  f: (a: Acc, b: T) => Acc,
+  f: SyncReducer<Acc, T>,
   acc: Acc,
   iterable: Iterable<T>,
 ): Acc {
@@ -16,7 +17,7 @@ function sync<T, Acc>(
 }
 
 async function async<T, Acc>(
-  f: (a: Acc, b: T) => Acc,
+  f: SyncReducer<Acc, T>,
   acc: Promise<Acc>,
   iterable: AsyncIterable<T>,
 ) {
@@ -66,11 +67,8 @@ async function async<T, Acc>(
  * ); // 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(
@@ -114,48 +112,42 @@ function reduce<T extends readonly [], Acc>(
   iterable: T,
 ): Acc;
 
-function reduce<T>(f: (acc: T, value: T) => T, iterable: Iterable<T>): T;
+function reduce<T>(f: SyncReducer<T, T>, iterable: Iterable<T>): T;
 
-function reduce<T, Acc>(
-  f: (acc: Acc, value: T) => Acc,
-  iterable: Iterable<T>,
-): Acc;
+function reduce<T, Acc>(f: SyncReducer<Acc, T>, iterable: Iterable<T>): Acc;
 
 function reduce<T, Acc>(
-  f: (acc: Acc, value: T) => Acc,
+  f: SyncReducer<Acc, T>,
   seed: Acc,
   iterable: Iterable<T>,
 ): Acc;
 
 function reduce<T>(
-  f: (acc: T, value: T) => T,
+  f: SyncReducer<T, T>,
   iterable: AsyncIterable<T>,
 ): Promise<T>;
 
 function reduce<T, Acc>(
-  f: (acc: Acc, value: T) => Acc | Promise<Acc>,
+  f: AsyncReducer<Acc, T>,
   seed: Acc | Promise<Acc>,
   iterable: AsyncIterable<T>,
 ): Promise<Acc>;
 
 function reduce<T, Acc>(
-  f: (acc: Acc, value: T) => Acc | Promise<Acc>,
+  f: AsyncReducer<Acc, T>,
   iterable: AsyncIterable<T>,
 ): Promise<Acc>;
 
 function reduce<T extends Iterable<unknown> | AsyncIterable<unknown>>(
-  f: (
-    acc: IterableInfer<T>,
-    value: IterableInfer<T>,
-  ) => IterableInfer<T> | Promise<IterableInfer<T>>,
-): (iterable: T) => ReturnValueType<T, IterableInfer<T>>;
+  f: AsyncReducer<IterableInfer<T>, IterableInfer<T>>,
+): (iterable: T) => ReturnValueType<T>;
 
 function reduce<T extends Iterable<unknown> | AsyncIterable<unknown>, Acc>(
-  f: (acc: Acc, value: IterableInfer<T>) => Acc | Promise<Acc>,
+  f: AsyncReducer<Acc, IterableInfer<T>>,
 ): (iterable: T) => ReturnValueType<T, Acc>;
 
 function reduce<T extends Iterable<unknown> | AsyncIterable<unknown>, Acc>(
-  f: (acc: Acc, value: IterableInfer<T>) => Acc,
+  f: SyncReducer<Acc, IterableInfer<T>>,
   seed?: Acc | Iterable<IterableInfer<T>> | AsyncIterable<IterableInfer<T>>,
   iterable?: Iterable<IterableInfer<T>> | AsyncIterable<IterableInfer<T>>,
 ): Acc | Promise<Acc> | ((iterable: T) => ReturnValueType<T, Acc>) {
@@ -195,7 +187,9 @@ function reduce<T extends Iterable<unknown> | AsyncIterable<unknown>, 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)) {
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, T> = (acc: Acc, value: T) => Acc;
+
+export type AsyncReducer<Acc, T> = (acc: Acc, value: T) => Acc | Promise<Acc>;


Test patch:
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<typeof homoSyncFnIterable, number, Test.Pass>(),
+  check<typeof homoSyncFnAsyncIterable, Promise<number>, Test.Pass>(),
+  check<typeof homoAsyncFnIterable, Promise<number>, Test.Pass>(),
+  check<typeof homoAsyncFnAsyncIterable, Promise<number>, Test.Pass>(),
+  check<typeof heteroSyncFnIterable, number, Test.Pass>(),
+  check<typeof heteroSyncFnAsyncIterable, Promise<number>, Test.Pass>(),
+  check<typeof heteroAsyncFnIterable, Promise<number>, Test.Pass>(),
+  check<typeof heteroAsyncFnAsyncIterable, Promise<number>, Test.Pass>(),
+  check<typeof homoPipe, number, Test.Pass>(),
+  check<typeof heteroPipe, number, Test.Pass>(),
+  check<typeof homoPipeAsync, Promise<number>, Test.Pass>(),
+  check<typeof heteroPipeAsync, Promise<number>, Test.Pass>(),
+  check<typeof homoPipeAsyncPromise, Promise<number>, Test.Pass>(),
+  check<typeof heteroPipePromise, Promise<number>, Test.Pass>(),
+]);