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

✨ Add Future monad #32

Merged
merged 6 commits into from
Sep 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
21 changes: 1 addition & 20 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@
- ✅ Provide tests for your changes.
- 📝 Use descriptive commit messages.
- 📗 Update any related documentation and include any relevant screenshots.

NOTE: Pull Requests from forked repositories will need to be reviewed by
a Forem Team member before any CI builds will run. Once your PR is approved
with a `/ci` reply to the PR, it will be allowed to run subsequent builds without
manual approval.
-->

## What type of PR is this? (check all applicable)
Expand All @@ -31,20 +26,6 @@
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and
context. List any dependencies that are required for this change. -->

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. We like to follow [Github's guidance on linking issues to pull requests](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).

For example having the text: "closes #1234" would connect the current pull
request to issue 1234. And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #
- Closes #

## Added/updated tests?

_We encourage you to keep the quality of the code by creating test._
Expand All @@ -56,4 +37,4 @@ _We encourage you to keep the quality of the code by creating test._

## [optional] What gif best describes this PR or how it makes you feel?

![alt_text](gif_link)
![alt_text](https://example.com/image.gif)
87 changes: 82 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,18 @@ It is a work in progress and the first monad implemented is the Either monad.
* [Checking if an Option is Some or None](#checking-if-an-option-is-some-or-none)
* [Try Monad](#try-monad)
* [Usage](#usage-2)
* [Using `map`](#using-map-1)
* [Using `flatMap`](#using-flatmap-1)
* [Matching a Try](#matching-a-try)
* [Handling errors in Infrastructure code](#handling-errors-in-infrastructure-code)
* [Checking if a Try is Success or Failure](#checking-if-a-try-is-success-or-failure)
* [Using `map`](#using-map-1)
* [Using `flatMap`](#using-flatmap-1)
* [Matching a Try](#matching-a-try)
* [Handling errors in Infrastructure code](#handling-errors-in-infrastructure-code)
* [Checking if a Try is Success or Failure](#checking-if-a-try-is-success-or-failure)
* [Future Monad](#future-monad)
* [Usage](#usage-3)
* [Creating a Future](#creating-a-future)
* [Mapping over a Future](#mapping-over-a-future)
* [Using `flatMap`](#using-flatmap-2)
* [Using `map`](#using-map-2)
* [Evaluate a Future](#evaluate-a-future)
<!-- TOC -->

## Installation
Expand Down Expand Up @@ -385,3 +392,73 @@ const failure = Try.execute(() => {
});
failure.isFailure(); // true
```

## Future Monad

The `Future` monad represents a computation that may be executed asynchronously.

### Usage

#### Creating a Future

You can create a `Future` using the static method `Future.of`.

```typescript
import { Future } from '@leanmind/monads';

const future = Future.of(() => Promise.resolve(42));
```

#### Mapping over a Future

You can use the `map` or `flatMap` method to transform the computed value inside a `Future`. The operation will not
execute the transformation (_lazy evaluation_) until `complete` method is called.

##### Using `flatMap`

```typescript
import { Future } from '@leanmind/monads';

const future = Future.of(() => Promise.resolve(42))
.flatMap(x => Future.of(() => Promise.resolve(x + 1)))
.complete(
x => console.log(x),
err => console.error(err)
); // 43
```

##### Using `map`

```typescript
import { Future } from '@leanmind/monads';

const future = Future.of(() => Promise.resolve(42))
.map(x => x + 1)
.complete(
x => console.log(x),
err => console.error(err)
); // 43
```

#### Evaluate a Future

You can evaluate a `Future` using the `complete` method. The `complete` method takes two functions as arguments:
one for the success case and one for the failure case.

```typescript
import { Future } from '@leanmind/monads';

const successFuture = Future.of(() => Promise.resolve(42));

await successFuture.complete(
x => console.log(x),
err => console.error(err)
); // 42

const failureFuture = Future.of(() => Promise.reject(new Error('Error')));

await failureFuture.complete(
x => console.log(x),
err => console.error(err)
); // Error('Error')
```
3 changes: 3 additions & 0 deletions src/complete/completable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface Completable<T> {
complete<S>(onSuccess: (value: T) => S, onFailure: (error: Error) => S): Promise<S>;
}
1 change: 1 addition & 0 deletions src/complete/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Completable } from './completable';
2 changes: 1 addition & 1 deletion src/either/either.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { Either, Right } from './either';
import { Either } from './either';
import { Option } from '../option';

describe('Either monad', () => {
Expand Down
50 changes: 25 additions & 25 deletions src/either/either.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ abstract class Either<L, R> implements Monad<R>, Matchable<R, L> {
}
}

abstract map<T>(f: (r: R) => T): Either<L, T>;
abstract map<T>(transform: (r: R) => T): Either<L, T>;

abstract mapLeft<T>(f: (l: L) => T): Either<T, R>;
abstract mapLeft<T>(transform: (l: L) => T): Either<T, R>;

abstract flatMap<T>(f: (r: R) => Either<L, T>): Either<L, T>;
abstract flatMap<T>(transform: (r: R) => Either<L, T>): Either<L, T>;

abstract flatMapLeft<T>(f: (l: L) => Either<T, R>): Either<T, R>;
abstract flatMapLeft<T>(transform: (l: L) => Either<T, R>): Either<T, R>;

abstract match<T>(ifRight: (r: R) => T, ifLeft: (l: L) => T): T;

Expand All @@ -45,67 +45,67 @@ class Left<L, R> extends Either<L, R> {
super();
}

map<T>(_: (r: R) => T): Either<L, T> {
map(_: (r: never) => never): Either<L, never> {
return new Left(this.value);
}

mapLeft<T>(f: (l: L) => T): Either<T, R> {
return new Left(f(this.value));
mapLeft<T>(transform: (l: L) => T): Either<T, never> {
return new Left(transform(this.value));
}

flatMap<T>(_: (r: never) => Either<L, T>): Either<L, T> {
flatMap(_: (r: never) => Either<L, never>): Either<L, never> {
return new Left(this.value);
}

flatMapLeft<T>(transform: (l: L) => Either<T, never>): Either<T, never> {
return transform(this.value);
}

match<T>(_: (_: never) => never, ifLeft: (l: L) => T): T {
return ifLeft(this.value);
}

isLeft(): this is Left<L, R> {
isLeft(): this is Left<L, never> {
return true;
}

isRight(): this is Right<L, R> {
isRight(): this is Right<L, never> {
return false;
}

flatMapLeft<T>(f: (l: L) => Either<T, R>): Either<T, R> {
return f(this.value);
}
}

class Right<L, R> extends Either<L, R> {
constructor(private value: R) {
super();
}

map<T>(f: (r: R) => T): Either<L, T> {
return new Right(f(this.value));
map<T>(transform: (r: R) => T): Either<never, T> {
return new Right(transform(this.value));
}

mapLeft<T>(_: (l: L) => T): Either<T, R> {
mapLeft(_: (l: L) => never): Either<never, R> {
return new Right(this.value);
}

flatMap<T>(f: (r: R) => Either<L, T>): Either<L, T> {
return f(this.value);
flatMap<T>(transform: (r: R) => Either<never, T>): Either<never, T> {
return transform(this.value);
}

flatMapLeft(_: (l: never) => Either<never, R>): Either<never, R> {
return new Right(this.value);
}

match<T>(ifRight: (r: R) => T, _: (_: never) => never): T {
return ifRight(this.value);
}

isLeft(): this is Left<L, R> {
isLeft(): this is Left<never, R> {
return false;
}

isRight(): this is Right<L, R> {
isRight(): this is Right<never, R> {
return true;
}

flatMapLeft<T>(_: (l: never) => Either<T, R>): Either<T, R> {
return new Right(this.value);
}
}

export { Either, Right, Left };
84 changes: 84 additions & 0 deletions src/future/future.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { assert, describe, expect, it, vi } from 'vitest';
import { Future } from './future';

describe('Future monad', () => {
it.each([
{
completionType: 'success',
action: () => Promise.resolve(2),
expected: {
ifSuccess: (value: number) => expect(value).toBe(2),
ifFailure: (_) => assert.fail('Error should not be thrown'),
},
},
{
completionType: 'failure',
action: () => Promise.reject<number>(new Error('Error')),
expected: {
ifSuccess: () => assert.fail('Value should not be emitted'),
ifFailure: (error: Error) => expect(error).toEqual(new Error('Error')),
},
},
])('should handle $completionType completion', async ({ action, expected }) => {
const future = Future.of(action);
await future.complete(expected.ifSuccess, expected.ifFailure);
});

myugen marked this conversation as resolved.
Show resolved Hide resolved
it.each([
{
completionType: 'Success',
future: Future.of(() => Promise.resolve(2)),
expected: {
ifSuccess: (value: number) => expect(value).toBe(4),
ifFailure: (_) => assert.fail('Error should not be thrown'),
},
},
{
completionType: 'Failure',
future: Future.of(() => Promise.reject<number>(new Error('Error'))),
expected: {
ifSuccess: () => assert.fail('Value should not be emitted'),
ifFailure: (error: Error) => expect(error).toEqual(new Error('Error')),
},
},
])('$completionType completion should handle map correctly', async ({ future, expected }) => {
const actual = future.map((value) => value * 2);
await actual.complete(expected.ifSuccess, expected.ifFailure);
});

myugen marked this conversation as resolved.
Show resolved Hide resolved
it.each([
{
completionType: 'Success',
future: Future.of(() => Promise.resolve(2)),
expected: {
ifSuccess: (value: number) => expect(value).toBe(4),
ifFailure: (error: Error) => assert.fail('Error should not be thrown'),
},
},
{
completionType: 'Failure',
future: Future.of(() => Promise.reject<number>(new Error('Error'))),
expected: {
ifSuccess: () => assert.fail('Value should not be emitted'),
ifFailure: (error: Error) => expect(error).toEqual(new Error('Error')),
},
},
])('$completionType completion should handle flatMap correctly', async ({ future, expected }) => {
const actual = future.flatMap((value) => Future.of(() => Promise.resolve(value * 2)));
await actual.complete(expected.ifSuccess, expected.ifFailure);
});

it('should not execute async action when a mapping is performed', async () => {
const asyncAction = vi.fn(async () => 2);
const future = Future.of(asyncAction);
future.map((value) => value * 2);
expect(asyncAction).not.toHaveBeenCalled();
});

it('should not execute async action when a flat mapping is performed', async () => {
const asyncAction = vi.fn(async () => 2);
const future = Future.of(asyncAction);
future.flatMap((value) => Future.of(() => Promise.resolve(value * 2)));
expect(asyncAction).not.toHaveBeenCalled();
});
});
24 changes: 24 additions & 0 deletions src/future/future.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Monad } from '../monad';
import { Completable } from '../complete';

class Future<T> implements Monad<T>, Completable<T> {
private constructor(private readonly action: () => Promise<T>) {}
myugen marked this conversation as resolved.
Show resolved Hide resolved

static of<T>(action: () => Promise<T>): Future<T> {
return new Future(action);
}

map<U>(transform: (value: T) => U): Future<U> {
return new Future<U>(() => this.action().then(transform));
}

flatMap<U>(transform: (value: T) => Future<U>): Future<U> {
return new Future<U>(() => this.action().then((value) => transform(value).action()));
}

complete<S>(onSuccess: (value: T) => S, onFailure: (error: Error) => S): Promise<S> {
return this.action().then(onSuccess).catch(onFailure);
}
}

export { Future };
1 change: 1 addition & 0 deletions src/future/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Future } from './future';
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './either';
export * from './future';
export * from './option';
export * from './try';
Loading