Skip to content

Commit 527bc13

Browse files
committed
Added Traverse applicative
1 parent befd021 commit 527bc13

7 files changed

Lines changed: 198 additions & 15 deletions

File tree

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@assemblerjs/core",
3-
"version": "0.8.31",
3+
"version": "0.9.1",
44
"main": "./dist/index.js",
55
"module": "./dist/index.mjs",
66
"types": "./dist/index.d.ts",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './traverse.applicative';
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { asyncMap } from '@/loop.utils';
3+
import { Task } from '../monad-like/task.monad';
4+
import { Maybe } from '../monad-like/maybe.monad';
5+
import { Traverse } from './traverse.applicative';
6+
7+
describe('Traverse', () => {
8+
it('should apply functions to values and collect results', async () => {
9+
const traverse = Traverse.of([(x: number) => x * 2, (x: number) => x + 1]);
10+
const results = await traverse.ap([1, 2, 3]);
11+
expect(results).toEqual([2, 4, 6, 2, 3, 4]);
12+
});
13+
14+
it('should handle errors during function application', async () => {
15+
const traverse = Traverse.of([
16+
(x: number) => x * 2,
17+
(_: number) => {
18+
throw new Error('Test Error');
19+
},
20+
]);
21+
const results = await traverse.ap([1, 2]);
22+
expect(results).toEqual([
23+
2,
24+
4,
25+
new Error('Test Error'),
26+
new Error('Test Error'),
27+
]);
28+
});
29+
30+
it('should handle async functions', async () => {
31+
const traverse = Traverse.of([
32+
async (x: number) => x * 2,
33+
async (x: number) => x + 1,
34+
]);
35+
const results = await traverse.ap([1, 2]);
36+
expect(results).toEqual([2, 4, 2, 3]);
37+
});
38+
39+
it('should work with Task monad', async () => {
40+
const task = (x: number) => Task.of(() => 42 + x);
41+
const traverse = Traverse.of([task]);
42+
const apply = await traverse.ap([1]);
43+
const result = await asyncMap(apply)(async (fn) =>
44+
(await fn.fork()).unwrap()
45+
);
46+
expect(result).toEqual([43]);
47+
});
48+
49+
it('should work with Maybe monad', async () => {
50+
const maybeFn = (x: number) => Maybe.of(x > 0 ? x * 2 : null);
51+
const traverse = Traverse.of([maybeFn]);
52+
const maybes: Array<Maybe<number | null>> = (await traverse.ap([
53+
1, -1, 2,
54+
])) as Maybe<number | null>[];
55+
const results = maybes.map((maybe) => maybe.unwrapOr(null));
56+
expect(results).toEqual([2, null, 4]);
57+
});
58+
59+
it('should map functions correctly', () => {
60+
const traverse = Traverse.of([(x: number) => x * 2, (x: number) => x + 1]);
61+
const mappedTraverse = traverse.map(
62+
(fn: Function) => (x: number) => fn(x) + 1
63+
);
64+
const results = mappedTraverse.getFunctions().map((fn) => fn(2));
65+
expect(results).toEqual([5, 4]);
66+
});
67+
68+
it('should chain multiple Traverse operations', () => {
69+
const traverse = Traverse.of([(x: number) => x * 2, (x: number) => x + 1]);
70+
const chainedTraverse = traverse.chain((fn: Function) =>
71+
Traverse.of([(x: number) => fn(x) + 3])
72+
);
73+
const results = chainedTraverse.getFunctions().map((fn) => fn(2));
74+
expect(results).toEqual([7, 6]);
75+
});
76+
77+
it('should call the function with undefined value if no values are provided', async () => {
78+
const traverse = Traverse.of([() => 2 * 2, () => 1 + 1]);
79+
const results = await traverse.ap();
80+
expect(results).toEqual([4, 2]);
81+
});
82+
83+
it('should call the function with undefined value and async functions in input', async () => {
84+
const traverse = Traverse.of([
85+
async () =>
86+
await new Promise<number>((resolve) =>
87+
setTimeout(() => resolve(2 * 2), 10)
88+
),
89+
async () =>
90+
await new Promise<number>((resolve) =>
91+
setTimeout(() => resolve(1 + 1), 20)
92+
),
93+
async () =>
94+
await new Promise<number | Error>((reject) =>
95+
setTimeout(() => reject(new Error('Test Error')), 30)
96+
),
97+
]);
98+
const results = await traverse.ap();
99+
expect(results).toEqual([4, 2, new Error('Test Error')]);
100+
});
101+
102+
it('should handle empty functions array', async () => {
103+
const traverse = Traverse.of([]);
104+
const results = await traverse.ap([1, 2, 3]);
105+
expect(results).toEqual([]);
106+
});
107+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { isAsync, isPromise } from '../type.validator';
2+
3+
/**
4+
* Traverse class implementing applicative functor pattern for arrays
5+
* Allows applying an array of functions to arrays of values, collecting results
6+
*/
7+
export class Traverse<T, U> {
8+
/**
9+
* Creates a new Traverse instance wrapping an array of functions.
10+
* Handles both sync and async functions.
11+
* @param functions Array of functions to wrap (sync or async)
12+
* @returns New Traverse instance
13+
*/
14+
static of<T, U>(
15+
functions: Array<(value: T) => U | Promise<U>>
16+
): Traverse<T, U | Promise<U>> {
17+
return new Traverse(functions);
18+
}
19+
20+
private constructor(
21+
private readonly functions: Array<(value: T) => U | Promise<U>>
22+
) {}
23+
24+
/**
25+
* Applies the wrapped functions to an array of values.
26+
* Handles both sync and async functions, returning a Promise of all results.
27+
* @param values Array of values to apply functions to
28+
* @returns Promise resolving to array of results from applying functions to values
29+
*/
30+
async ap(values?: T[]): Promise<U[]> {
31+
const results: U[] = [];
32+
const valuesToUse = values || [undefined as T];
33+
34+
for (const fn of this.functions) {
35+
for (const val of valuesToUse) {
36+
try {
37+
let res: U;
38+
if (isAsync(fn) || isPromise(fn)) {
39+
res = (await fn(val)) as U;
40+
} else {
41+
res = fn(val) as U;
42+
}
43+
results.push(res);
44+
} catch (err) {
45+
results.push(err as U);
46+
}
47+
}
48+
}
49+
50+
return results as U[];
51+
}
52+
53+
/**
54+
* Maps a function over the wrapped functions
55+
* @param fn Function to transform each function
56+
* @returns New Traverse instance with transformed functions
57+
*/
58+
map<V>(
59+
fn: (func: (value: T) => U | Promise<U>) => (value: T) => V | Promise<V>
60+
): Traverse<T, V | Promise<V>> {
61+
return new Traverse(this.functions.map(fn));
62+
}
63+
64+
/**
65+
* Gets the wrapped functions
66+
* @returns Array of wrapped functions
67+
*/
68+
getFunctions(): ((value: T) => U | Promise<U>)[] {
69+
return [...this.functions];
70+
}
71+
72+
/**
73+
* Chains multiple Traverse operations
74+
* @param fn Function that returns a Traverse instance
75+
* @returns Flattened Traverse instance
76+
*/
77+
chain<V>(
78+
fn: (func: (value: T) => U | Promise<U>) => Traverse<T, V | Promise<V>>
79+
): Traverse<T, V | Promise<V>> {
80+
const results: ((value: T) => V | Promise<V>)[] = [];
81+
for (const func of this.functions) {
82+
results.push(
83+
...(fn(func).getFunctions() as ((value: T) => V | Promise<V>)[])
84+
);
85+
}
86+
return new Traverse(results);
87+
}
88+
}

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './proxy.utils';
1212
export * from './string.formatter';
1313

1414
export * from './monad-like';
15+
export * from './applicative-like';
1516

1617
export * from './decorators/class/singleton.decorator';
1718
export * from './decorators/class/static-implements.decorator';

packages/core/src/monad-like/traversal.monad.spec.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/core/src/monad-like/traversal.monad.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)