Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement reduceLazy (#239) #245

Merged
merged 3 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>(
Phryxia marked this conversation as resolved.
Show resolved Hide resolved
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);
Comment on lines +101 to +104
Copy link
Contributor Author

@Phryxia Phryxia Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any idea to overcome this, and reduce also uses as any so I just simply suppress them.
Actually it's common to use anyscript(😆) for implementation if type signatures are sufficiently provided.

}

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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* You may want to use `reduceLazy` to use `seed`.
* You may want to use {@link https://fxts.dev/docs/reduceLazy | 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>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReturnValueType<T> is equivalent to ReturnValueType<T, IterableInfer<T>>. See ReturnValueType.ts.


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", () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it("should be occured error when the given `iterable` is an empty array and initial value is absent", () => {
it("should be occurred 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>(),
]);
Loading