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

Feature: Better docs and utility functions #12

Merged
merged 11 commits into from
Sep 21, 2022
25 changes: 14 additions & 11 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,46 @@
"version": "2.0.0",
"tasks": [
{
"label": "install",
"type": "npm",
"script": "install",
"group": "build",
"problemMatcher": [],
"label": "install",
"detail": "install"
"detail": "Installs all dependencies"
},
{
"label": "test",
"dependsOn": ["install"],
"type": "npm",
"script": "test",
"group": "test",
"problemMatcher": [],
"label": "test",
"detail": "jest"
"detail": "Runs the Jest tests"
},
{
"label": "test - watch",
"dependsOn": ["install"],
"type": "npm",
"script": "test:watch",
"group": "test",
"problemMatcher": [],
"label": "test - watch",
"detail": "jest"
"detail": "Runs the Jest tests in watch mode"
},
{
"label": "typescript - build",
"dependsOn": ["install"],
"type": "npm",
"script": "build",
"group": "build",
"problemMatcher": [],
"label": "npm: build",
"detail": "tsc"
"detail": "Runs the TypeScript compilation"
},
{
"label": "typescript - watch",
"dependsOn": ["install"],
"type": "npm",
"script": "start",
"problemMatcher": [],
"label": "npm: start",
"detail": "tsc --watch"
"detail": "Runs the TypeScript compilation in watch mode"
}
]
}
271 changes: 126 additions & 145 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,195 +11,174 @@ A TypeScript implementation of the C# library [CSharpFunctionalExtensions](https
- [Modeling Missing Data - The Maybe Monad](https://dev.to/seangwright/kentico-xperience-design-patterns-modeling-missing-data-the-maybe-monad-2c7i)
- [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/)

## Monads

Below are the monads included in this package and examples of their use (more examples of all monads and their methods can be found in the library unit tests).

### Maybe
## How to Use

`Maybe` represents a value that might or might not exist.

#### Some/None/From
### Core Monads

```typescript
const maybe = Maybe.some('apple');

console.log(maybe.hasValue); // true
console.log(maybe.hasNoValue); // false
console.log(maybe.getValueOrDefault('banana')); // 'apple'
console.log(maybe.getValueOrThrow()); // 'apple'
import {
Maybe,
MaybeAsync,
Result,
ResultAsync,
} from 'typescript-functional-extensions';
```

```typescript
const maybe = Maybe.none();

console.log(maybe.hasValue); // false
console.log(maybe.hasNoValue); // true
console.log(maybe.getValueOrDefault('banana')); // 'banana'
console.log(maybe.getValueOrThrow()); // throws Error 'No value'
```
### Utilities

```typescript
const maybe = Maybe.from(undefined);

console.log(maybe.hasValue); // false
console.log(maybe.hasNoValue); // true
console.log(maybe.getValueOrDefault('banana')); // 'banana'
console.log(maybe.getValueOrThrow()); // throws Error 'No value'
import {
never,
isDefined,
isSome,
isNone,
isFunction,
isPromise,
noop,
} from 'typescript-functional-extensions';
```

#### TryFirst

```typescript
const maybe = Maybe.tryFirst(['apple', 'banana']);

console.log(maybe.getValueOrThrow()); // 'apple'
import {
zeroAsNone,
emptyStringAsNone,
emptyOrWhiteSpaceStringAsNone,
} from 'typescript-functional-extensions';
```

```typescript
const maybe = Maybe.tryFirst(['apple', 'banana'], (fruit) => fruit.length > 6);

console.log(maybe.getValueOrThrow()); // throws Error 'No value'
```

#### TryLast

```typescript
const maybe = Maybe.tryLast(
['apple', 'banana', 'mango'],
(fruit) => fruit.length === 5
);

console.log(maybe.getValueOrThrow()); // 'mango'
```

```typescript
const maybe = Maybe.tryLast(
['apple', 'banana', 'mango'],
(fruit) => fruit === 'pear'
);
## Monads

console.log(maybe.getValueOrThrow()); // throws Error 'No value'
```
Below are the monads included in this package and examples of their use.

#### Map
More examples of all monads and their methods can be found in the library unit tests or in the dedicated documentation files for each type.

```typescript
const maybe = Maybe.some({ type: 'fruit', name: 'apple' })
.map(({ type }) => ({ type, name: 'banana' }))
.map((food) => food.name)
.map((name) => name.length);
### Maybe

console.log(maybe.getValueOrThrow()); // 6
```
`Maybe` represents a value that might or might not exist. You can use it to declaratively describe a process (series of steps) without having to check if there is a value present.

```typescript
type Food = {
type: string;
name: string;
type Employee = {
email: string;
firstName: string;
lastName: string;
manager: Employee | undefined;
};

const maybe = Maybe.none<Food>()
.map(({ type }) => ({ type, name: 'banana' }))
.map((food) => food.name)
.map((name) => name.length);

console.log(maybe.getValueOrThrow()); // throws Error 'No value'
```

#### Match

```typescript
const maybe = Maybe.some({ type: 'fruit', name: 'apple' })
.map(({ type }) => ({ type, name: 'banana' }))
.map((food) => food.name)
.map((name) => name.length)
.match({
some: (number) => console.log(number),
none: () => console.log('None!'),
}); // 6
```

```typescript
type Food = {
type: string;
name: string;
};
function yourBusinessProcess(): Employee[] {
// ...
}

const maybe = Maybe.none<Food>()
.map(({ type }) => ({ type, name: 'banana' }))
.map((food) => food.name)
.map((name) => name.length)
const employees = yourBusinessProcess();

Maybe.tryFirst(employees)
.tap(({ firstName, lastName, email }) =>
console.log(`Found Employee: ${firstName} ${lastName}, ${email}`))
.bind(employee =>
Maybe.from(employee.manager)
.or({
email: 'supervisor@business.com',
firstName: 'Company',
lastName: 'Supervisor',
manager: undefined
})
.map(manager => ({ manager, employee }))
)
.match({
some: (number) => console.log(number),
none: () => console.log('None!'),
}); // None!
some(attendees => scheduleMeeting(attendees.manager, attendees.employee)),
none(() => console.log(`The business process did not return any employees`))
});
seangwright marked this conversation as resolved.
Show resolved Hide resolved
```

#### Pipe
1. `tryFirst` finds the first employee in the array and wraps it in a `Maybe`. If the array is empty, a `Maybe` with no value is returned.
1. `tap`'s callback is only called if an employee was found and logs out that employees' information.
seangwright marked this conversation as resolved.
Show resolved Hide resolved
1. `bind`'s callback is only called if an employee was found and converts the `Maybe` wrapping it into to another `Maybe`.
1. `from` wraps the employee's manager in a `Maybe`. If the employee has no manager, a `Maybe` with no value is returned.
1. `or` supplies a fallback in the case that the employee has no manager so that as long as an employee was originally found, all the following operations will execute.
1. `map` converts the manager to a new object which contains both the manager and employee.
1. `match` executes its `some` function if an employee was originally found and that employee has a manager. Since we supplied a fallback manager with `or`, the `some` function of `match` will execute if we found an employee. The `none` function of `match` executes if we didn't find any employees.

```typescript
// custom-operators.ts
import { logger, LogLevel } from 'logger';

export function log<TValue>(
messageCreator: FunctionOfTtoK<TValue, string>,
logLevel: LogLevel = 'debug'
): MaybeOpFn<TValue, TValue> {
return (maybe) => {
if (maybe.hasValue) {
logger.log(messageCreator(maybe.getValueOrThrow()), logLevel);
} else {
logger.error('No value found!');
}

return maybe;
};
}

// app.ts
import { log } from './custom-operators.ts';

const maybe = Maybe.some('apple')
.pipe(log((f) => `My fruit is ${f}`, 'information'))
.map((f) => `${f} and banana`)
.pipe(log((f) => `Now I have ${f}`));
```
See more examples of Maybe [in the docs](./docs/maybe.md) or [in the tests](./test/maybe).

### MaybeAsync

`MaybeAsync` represents a future value (`Promise`) that might or might not exist.

```typescript
function getFruit(day): Promise<string> {
return Promise.resolve('apple');
}
`MaybeAsync` works just like `Maybe`, but since it is asynchronous, its methods accept a `Promise<T>` in most cases and all of its value accessing methods/getters return a `Promise<T>`.

const maybeAsync = MaybeAsync.from(getFruit());

const maybe = maybeAsync.toPromise();

console.log(maybe.getValueOrThrow()); // 'apple'
```
See more examples of Maybe [in the docs](./docs/maybeAsync.md) or [in the tests](./test/maybeAsync).
seangwright marked this conversation as resolved.
Show resolved Hide resolved

### Result

`Result` represents a successful or failed operation.
`Result` represents a successful or failed operation. You can use it to declaratively define a process without needing to check if previous steps succeeded or failed. It can replace processes that use throwing errors and `try`/`catch` to control the flow of the application, or processes where errors and data are returned from every function.

```typescript
const successfulResult = Result.success('apple');
type Employee = {
id: number;
email: string;
firstName: string;
lastName: string;
managerId: number | undefined;
};

function getEmployee(employeeId): Employee | undefined {
const employee = getEmployee(employeeId);

console.log(successfulResult.getValueOrThrow()); // 'apple'
if (!employee) {
throw Error(`Could not find employee ${employeeId}!`);
}

const failedResult = Result.failure('no fruit');
return employee;
}

console.log(failedResult.getErrorOrThrow()); // 'no fruit'
Result.try(
() => getEmployee(42),
(error) => `Retrieving the employee failed: ${error}`
)
.ensure(
(employee) => employee.email.endsWith('@business.com'),
({ firstName, lastName }) =>
`Employee ${firstName} ${lastName} is a contractor and not a full employee`
)
.bind(({ firstName, lastName, managerId }) =>
Maybe.from(managerId).toResult(
`Employee ${firstName} ${lastName} does not have a manager`
)
)
.map((managerId) => ({
managerId,
employeeFullName: `${firstName} ${lastName}`,
}))
.bind(({ managerId, employeeFullName }) =>
Result.try(
() => getEmployee(managerId),
(error) => `Retrieving the manager failed: ${error}`
).map((manager) => ({ manager, employeeFullName }))
)
.match({
success: ({ manager: { email }, employeeFullName }) =>
sendReminder(email, `Remember to say hello to ${employeeFullName}`),
failure: (error) => sendSupervisorAlert(error),
});
```

1. `try` executes the function to retrieve the employee, converting any thrown errors into a failed `Result` with the error message defined by the second parameter. If the employee is found, it returns a successful `Result`.
1. `ensure`'s callback is only called if an employee was successfully found. It checks if the employee works for the company by looking at their email address. If the address doesn't end in `@business.com`, a failed `Result` is returned with the error message defined in the second parameter. If the check passes, the original successful `Result` is returned.
1. `bind`'s callback is only called if the employee was found and works for the company. It converts the employee `Result` into another `Result`.
1. `toResult` converts a missing `managerId` into a failed `Result`. If there is a `managerId` value, it's converted into a successful `Result`.
1. `map`'s callback is only called if the `managerId` exists and converts the `managerId` into a new object to capture both the id and the employee's full name.
1. `bind`'s callback is only called if the original employee was found and that employee had a `managerId`. It converts the id and employee name into a new `Result`.
1. `try` now attempts to get the employee's manager and works the same as the first `try`.
1. `map`'s callback is only called if the original employee was found, has a `managerId` and that manager was also found. It converts the manager returned by `try` to a new object capturing both the manager and employee's name.
1. `match`'s `success` callback is only called if all the required information was retrieved and sends a reminder to the employee's manager. The `failure` callback is called if any of the required data could not be retrieved and sends an alert to the business supervisor with the error message.

See more examples of Maybe [in the docs](./docs/result.md) or [in the tests](./test/result).
seangwright marked this conversation as resolved.
Show resolved Hide resolved

### ResultAsync

`ResultAsync` represents a future result of an operation that either succeeds or fails.

`ResultAsync` works just like `Result`, but since it is asynchronous, its methods accept a `Promise<T>` in most cases and all of its value accessing methods/getters return a `Promise<T>`.

```typescript
function getLatestInventory(): Promise<{ apples: number }> {
return Promise.reject('connection failure');
Expand All @@ -216,5 +195,7 @@ const resultAsync = ResultAsync.from(async () => {

const result = await resultAsync.toPromise();

console.log(result.getErrorOrThrow()); // 'Could not retrieve inventory: connection failure
console.log(result.getErrorOrThrow()); // 'Could not retrieve inventory: connection failure'
```

See more examples of Maybe [in the docs](./docs/resultAsync.md) or [in the tests](./test/resultAsync).
seangwright marked this conversation as resolved.
Show resolved Hide resolved