Skip to content

Commit ddf5ef3

Browse files
[EVOL]: add lazy evaluation utils
1 parent 7af35bd commit ddf5ef3

14 files changed

+1070
-1
lines changed

benchmark-fp.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { performance } from 'perf_hooks';
22

33
import {
44
CanApply,
5+
LazyCall,
6+
LazyStream,
57
Ok,
68
Option,
9+
PageLazyStream,
710
SyncEffect,
811
compose,
912
composeTransducers,
@@ -266,3 +269,60 @@ benchmarks.push(
266269

267270
// **Display Benchmark Results**
268271
console.table(benchmarks);
272+
273+
// ** Benchmark for Lazy Evaluation **
274+
async function runBenchmarks() {
275+
const benchmarks: BenchmarkResult[] = [];
276+
277+
// ** Benchmark LazyCall **
278+
const lazyComputation = new LazyCall(() => {
279+
for (let i = 0; i < 1e7; i++) {
280+
// Do nothing
281+
} // Simulated heavy computation
282+
return 42;
283+
});
284+
285+
benchmarks.push(benchmark(() => lazyComputation.get(), 'LazyCall', 'First Evaluation'));
286+
287+
benchmarks.push(benchmark(() => lazyComputation.get(), 'LazyCall', 'Cached Evaluation'));
288+
289+
// ** Benchmark LazyStream **
290+
async function* asyncNumbers(start = 1) {
291+
let num = start;
292+
while (num <= 10000) {
293+
yield num++;
294+
}
295+
}
296+
297+
const stream = LazyStream.from(() => asyncNumbers())
298+
.map((x) => x * 2)
299+
.filter((x) => x % 3 === 0)
300+
.take(1000);
301+
302+
benchmarks.push(
303+
await benchmark(() => stream.toArray(), 'LazyStream', 'Lazy Mapping + Filtering + Taking'),
304+
);
305+
306+
// ** Benchmark PageLazyStream (Paginated API simulation) **
307+
async function* paginatedNumbers() {
308+
for (let i = 1; i <= 100; i++) {
309+
await new Promise((resolve) => setTimeout(resolve, 10)); // Simulate API delay
310+
yield Array.from({ length: 10 }, (_, j) => i * 10 + j);
311+
}
312+
}
313+
314+
const pageStream = PageLazyStream.from(() => paginatedNumbers())
315+
.map((page) => page.map((x) => x * 2))
316+
.filter((page) => page.some((x) => x % 5 === 0))
317+
.take(5);
318+
319+
benchmarks.push(
320+
await benchmark(() => pageStream.toArray(), 'PageLazyStream', 'Lazy Paginated Processing'),
321+
);
322+
323+
// ** Display Benchmark Results **
324+
console.table(benchmarks);
325+
}
326+
327+
// Run benchmarks
328+
runBenchmarks();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dsa-toolbox",
3-
"version": "2.0.4",
3+
"version": "2.1.0",
44
"description": "A powerful toolkit for data structures and algorithms in TypeScript, designed for optimal performance and versatility. The toolkit provides implementations of various data structures and algorithms, with a focus on search and sort operations, caching, and probabilistic data structures.",
55
"type": "module",
66
"main": "dist/index.js",

src/functional/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export {
1212
takeTransducer,
1313
} from './transducers/Transducers.ts';
1414
export * from './monads/index.ts';
15+
export * from './lazy/index.ts';
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
import { LazyCall } from './LazyCall.ts';
4+
5+
describe('LazyCall', () => {
6+
it('should evaluate the computation only once', () => {
7+
const spy = vi.fn(() => 42);
8+
const lazy = new LazyCall(spy);
9+
10+
expect(spy).not.toHaveBeenCalled();
11+
12+
const result1 = lazy.get();
13+
const result2 = lazy.get();
14+
15+
expect(spy).toHaveBeenCalledTimes(1);
16+
expect(result1).toBe(42);
17+
expect(result2).toBe(42);
18+
});
19+
20+
it('should apply transformations using map lazily', () => {
21+
const spy = vi.fn(() => 10);
22+
const lazy = new LazyCall(spy);
23+
const mapped = lazy.map((x) => x * 2);
24+
25+
expect(spy).not.toHaveBeenCalled(); // The computation should not run yet
26+
27+
const result = mapped.get();
28+
29+
expect(spy).toHaveBeenCalledTimes(1); // The original computation should run once
30+
expect(result).toBe(20);
31+
});
32+
33+
it('should apply flatMap and preserve laziness', () => {
34+
const spy = vi.fn(() => 5);
35+
const lazy = new LazyCall(spy);
36+
const flatMapped = lazy.flatMap((x) => new LazyCall(() => x * 3));
37+
38+
expect(spy).not.toHaveBeenCalled(); // Should not compute yet
39+
40+
const result = flatMapped.get();
41+
42+
expect(spy).toHaveBeenCalledTimes(1);
43+
expect(result).toBe(15);
44+
});
45+
46+
it('should work with multiple transformations and only compute once', () => {
47+
const spy = vi.fn(() => 3);
48+
const lazy = new LazyCall(spy).map((x) => x + 2).map((x) => x * 10);
49+
50+
expect(spy).not.toHaveBeenCalled(); // No evaluation yet
51+
52+
const result = lazy.get();
53+
54+
expect(spy).toHaveBeenCalledTimes(1);
55+
expect(result).toBe(50);
56+
});
57+
58+
it('should allow chaining of flatMap with dependent computations', () => {
59+
const lazy = new LazyCall(() => 5)
60+
.flatMap((x) => new LazyCall(() => x * 2))
61+
.flatMap((x) => new LazyCall(() => x + 3));
62+
63+
const result = lazy.get();
64+
65+
expect(result).toBe(13); // (5 * 2) + 3
66+
});
67+
68+
it('should correctly handle side effects only once', () => {
69+
let count = 0;
70+
const lazy = new LazyCall(() => {
71+
count++;
72+
return count;
73+
});
74+
75+
expect(count).toBe(0);
76+
77+
const first = lazy.get();
78+
const second = lazy.get();
79+
80+
expect(first).toBe(1);
81+
expect(second).toBe(1);
82+
expect(count).toBe(1);
83+
});
84+
});

src/functional/lazy/LazyCall.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* LazyCall Evaluation Wrapper.
3+
*
4+
* Allows delayed computation with functional transformations.
5+
*
6+
* @template T - The type of the computed value.
7+
*/
8+
export class LazyCall<T> {
9+
private computed: T | undefined;
10+
private isEvaluated = false;
11+
12+
/**
13+
* Constructs a new LazyCall instance with a computation.
14+
*
15+
* @param {() => T} computation - The function to compute the value lazily.
16+
*/
17+
constructor(private readonly computation: () => T) {}
18+
19+
/**
20+
* Computes the value **only once** and caches the result.
21+
*
22+
* @returns {T} The computed value.
23+
*/
24+
get(): T {
25+
if (!this.isEvaluated) {
26+
this.computed = this.computation(); // Compute and store the result
27+
this.isEvaluated = true; // Mark as evaluated to prevent re-execution
28+
}
29+
return this.computed!;
30+
}
31+
32+
/**
33+
* Transforms the stored value **without evaluating** it immediately.
34+
*
35+
* @template U - The new transformed type.
36+
* @param {(value: T) => U} fn - The transformation function.
37+
* @returns {LazyCall<U>} A new lazy instance with the transformation applied.
38+
*/
39+
map<U>(fn: (value: T) => U): LazyCall<U> {
40+
return new LazyCall(() => fn(this.get()));
41+
}
42+
43+
/**
44+
* Chains computations that return another LazyCall instance.
45+
*
46+
* @template U - The transformed type.
47+
* @param {(value: T) => LazyCall<U>} fn - The function returning a new LazyCall instance.
48+
* @returns {LazyCall<U>} A new lazy instance.
49+
*/
50+
flatMap<U>(fn: (value: T) => LazyCall<U>): LazyCall<U> {
51+
return new LazyCall(() => fn(this.get()).get());
52+
}
53+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import fc from 'fast-check';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import { LazyCall } from './LazyCall.ts';
5+
6+
describe('LazyCall - Property-Based Testing', () => {
7+
it('should always return the same value for the same computation', () => {
8+
fc.assert(
9+
fc.property(fc.integer(), (num) => {
10+
const lazy = new LazyCall(() => num);
11+
12+
expect(lazy.get()).toBe(num);
13+
expect(lazy.get()).toBe(num); // Ensure memoization
14+
}),
15+
);
16+
});
17+
18+
it('should only evaluate once regardless of how many times get() is called', () => {
19+
fc.assert(
20+
fc.property(fc.integer(), (num) => {
21+
const spy = vi.fn(() => num);
22+
const lazy = new LazyCall(spy);
23+
24+
lazy.get();
25+
lazy.get();
26+
lazy.get();
27+
28+
expect(spy).toHaveBeenCalledTimes(1);
29+
}),
30+
);
31+
});
32+
33+
it('should correctly apply map transformations', () => {
34+
fc.assert(
35+
fc.property(fc.integer(), fc.func(fc.integer()), (num, transformFn) => {
36+
const lazy = new LazyCall(() => num).map(transformFn);
37+
38+
expect(lazy.get()).toBe(transformFn(num));
39+
}),
40+
);
41+
});
42+
43+
it('should correctly apply flatMap transformations', () => {
44+
fc.assert(
45+
fc.property(fc.integer(), fc.func(fc.integer()), (num, transformFn) => {
46+
const lazy = new LazyCall(() => num).flatMap(
47+
(x) => new LazyCall(() => transformFn(x)),
48+
);
49+
50+
expect(lazy.get()).toBe(transformFn(num));
51+
}),
52+
);
53+
});
54+
55+
it('should preserve memoization after mapping', () => {
56+
fc.assert(
57+
fc.property(fc.integer(), fc.func(fc.integer()), (num, transformFn) => {
58+
const spy = vi.fn(() => num);
59+
const lazy = new LazyCall(spy).map(transformFn);
60+
61+
const result1 = lazy.get();
62+
const result2 = lazy.get();
63+
64+
expect(spy).toHaveBeenCalledTimes(1);
65+
expect(result1).toBe(result2);
66+
}),
67+
);
68+
});
69+
70+
it('should preserve memoization after flatMapping', () => {
71+
fc.assert(
72+
fc.property(fc.integer(), fc.func(fc.integer()), (num, transformFn) => {
73+
const spy = vi.fn(() => num);
74+
const lazy = new LazyCall(spy).flatMap((x) => new LazyCall(() => transformFn(x)));
75+
76+
const result1 = lazy.get();
77+
const result2 = lazy.get();
78+
79+
expect(spy).toHaveBeenCalledTimes(1);
80+
expect(result1).toBe(result2);
81+
}),
82+
);
83+
});
84+
85+
it('should chain multiple maps correctly', () => {
86+
fc.assert(
87+
fc.property(
88+
fc.integer(),
89+
fc.func(fc.integer()),
90+
fc.func(fc.integer()),
91+
(num, fn1, fn2) => {
92+
const lazy = new LazyCall(() => num).map(fn1).map(fn2);
93+
94+
expect(lazy.get()).toBe(fn2(fn1(num)));
95+
},
96+
),
97+
);
98+
});
99+
100+
it('should chain multiple flatMaps correctly', () => {
101+
fc.assert(
102+
fc.property(
103+
fc.integer(),
104+
fc.func(fc.integer()),
105+
fc.func(fc.integer()),
106+
(num, fn1, fn2) => {
107+
const lazy = new LazyCall(() => num)
108+
.flatMap((x) => new LazyCall(() => fn1(x)))
109+
.flatMap((x) => new LazyCall(() => fn2(x)));
110+
111+
expect(lazy.get()).toBe(fn2(fn1(num)));
112+
},
113+
),
114+
);
115+
});
116+
});

0 commit comments

Comments
 (0)