-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
6 changed files
with
284 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>(), | ||
]); |