Skip to content

Commit 3eee0ab

Browse files
committed
feat(monad): add future monad
1 parent 0a2e93f commit 3eee0ab

File tree

7 files changed

+115
-8
lines changed

7 files changed

+115
-8
lines changed

src/complete/completable.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Completable<T> {
2+
complete<S>(onSuccess: (value: T) => S, onFailure: (error: Error) => S): Promise<S>;
3+
}

src/complete/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Completable } from './completable';

src/either/either.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@ abstract class Either<L, R> implements Monad<R>, Matchable<R, L> {
1717
);
1818
}
1919

20-
static complete<R>(completable: Completable<R>): AsyncEither<Error, R> {
21-
return completable.complete<Either<Error, R>>(
22-
(value: R) => Either.right(value),
23-
(error: Error) => Either.left(error)
24-
);
25-
}
26-
2720
static catch<T>(execute: () => T): Either<Error, T> {
2821
try {
2922
return Either.right(execute());
@@ -115,4 +108,4 @@ class Right<L, R> extends Either<L, R> {
115108
}
116109
}
117110

118-
export { Either, Right, Left, AsyncEither };
111+
export { Either, Right, Left };

src/future/future.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { assert, describe, expect, it, vi } from 'vitest';
2+
import { Future } from './future';
3+
4+
describe('Future monad', () => {
5+
it.each([
6+
{
7+
completionType: 'success',
8+
action: () => Promise.resolve(2),
9+
expected: {
10+
ifSuccess: (value: number) => expect(value).toBe(2),
11+
ifFailure: (_) => assert.fail('Error should not be thrown'),
12+
},
13+
},
14+
{
15+
completionType: 'failure',
16+
action: () => Promise.reject<number>(new Error('Error')),
17+
expected: {
18+
ifSuccess: () => assert.fail('Value should not be emitted'),
19+
ifFailure: (error: Error) => expect(error).toEqual(new Error('Error')),
20+
},
21+
},
22+
])('should handle $completionType completion', async ({ action, expected }) => {
23+
const future = Future.of(action);
24+
await future.complete(expected.ifSuccess, expected.ifFailure);
25+
});
26+
27+
it.each([
28+
{
29+
completionType: 'Success',
30+
future: Future.of(() => Promise.resolve(2)),
31+
expected: {
32+
ifSuccess: (value: number) => expect(value).toBe(4),
33+
ifFailure: (_) => assert.fail('Error should not be thrown'),
34+
},
35+
},
36+
{
37+
completionType: 'Failure',
38+
future: Future.of(() => Promise.reject<number>(new Error('Error'))),
39+
expected: {
40+
ifSuccess: () => assert.fail('Value should not be emitted'),
41+
ifFailure: (error: Error) => expect(error).toEqual(new Error('Error')),
42+
},
43+
},
44+
])('$completionType completion should handle map correctly', async ({ future, expected }) => {
45+
const actual = future.map((value) => value * 2);
46+
await actual.complete(expected.ifSuccess, expected.ifFailure);
47+
});
48+
49+
it.each([
50+
{
51+
completionType: 'Success',
52+
future: Future.of(() => Promise.resolve(2)),
53+
expected: {
54+
ifSuccess: (value: number) => expect(value).toBe(4),
55+
ifFailure: (error: Error) => assert.fail('Error should not be thrown'),
56+
},
57+
},
58+
{
59+
completionType: 'Failure',
60+
future: Future.of(() => Promise.reject<number>(new Error('Error'))),
61+
expected: {
62+
ifSuccess: () => assert.fail('Value should not be emitted'),
63+
ifFailure: (error: Error) => expect(error).toEqual(new Error('Error')),
64+
},
65+
},
66+
])('$completionType completion should handle flatMap correctly', async ({ future, expected }) => {
67+
const actual = future.flatMap((value) => Future.of(() => Promise.resolve(value * 2)));
68+
await actual.complete(expected.ifSuccess, expected.ifFailure);
69+
});
70+
71+
it('should not execute async action when a mapping is performed', async () => {
72+
const asyncAction = vi.fn(async () => 2);
73+
const future = Future.of(asyncAction);
74+
future.map((value) => value * 2);
75+
expect(asyncAction).not.toHaveBeenCalled();
76+
});
77+
78+
it('should not execute async action when a flat mapping is performed', async () => {
79+
const asyncAction = vi.fn(async () => 2);
80+
const future = Future.of(asyncAction);
81+
future.flatMap((value) => Future.of(() => Promise.resolve(value * 2)));
82+
expect(asyncAction).not.toHaveBeenCalled();
83+
});
84+
});

src/future/future.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Monad } from '../monad';
2+
import { Completable } from '../complete';
3+
4+
class Future<T> implements Monad<T>, Completable<T> {
5+
private constructor(private readonly action: () => Promise<T>) {}
6+
7+
static of<T>(action: () => Promise<T>): Future<T> {
8+
return new Future(action);
9+
}
10+
11+
map<U>(transform: (value: T) => U): Future<U> {
12+
return new Future<U>(() => this.action().then(transform));
13+
}
14+
15+
flatMap<U>(transform: (value: T) => Future<U>): Future<U> {
16+
return new Future<U>(() => this.action().then((value) => transform(value).action()));
17+
}
18+
19+
complete<S>(onSuccess: (value: T) => S, onFailure: (error: Error) => S): Promise<S> {
20+
return this.action().then(onSuccess).catch(onFailure);
21+
}
22+
}
23+
24+
export { Future };

src/future/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Future } from './future';

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './either';
2+
export * from './future';
23
export * from './option';
34
export * from './try';

0 commit comments

Comments
 (0)