Skip to content

Commit

Permalink
feat: add reduceLazy (#246)
Browse files Browse the repository at this point in the history
* refactor: extract type of function arg in `reduce`

* feat: implement `reduceLazy` (#239)

* docs: add jsdoc for `reduceLazy`

---------

Co-authored-by: Phryxia <[email protected]>
  • Loading branch information
ppeeou and Phryxia authored Feb 18, 2024
1 parent 813e0fb commit 8a8a7bd
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 24 deletions.
2 changes: 2 additions & 0 deletions src/Lazy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -70,6 +71,7 @@ export {
prepend,
range,
reject,
reduceLazy,
repeat,
reverse,
scan,
Expand Down
107 changes: 107 additions & 0 deletions src/Lazy/reduceLazy.ts
Original file line number Diff line number Diff line change
@@ -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;
42 changes: 18 additions & 24 deletions src/reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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>,
) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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>) {
Expand Down Expand Up @@ -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)) {
Expand Down
3 changes: 3 additions & 0 deletions src/types/Reducer.ts
Original file line number Diff line number Diff line change
@@ -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>;
79 changes: 79 additions & 0 deletions test/Lazy/reduceLazy.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
75 changes: 75 additions & 0 deletions type-check/Lazy/reduceLazy.test.ts
Original file line number Diff line number Diff line change
@@ -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>(),
]);

0 comments on commit 8a8a7bd

Please sign in to comment.