Skip to content

Commit

Permalink
Update the Result monad example
Browse files Browse the repository at this point in the history
The new example includes methods added in the v0.10.0
  • Loading branch information
amikheychik committed Dec 1, 2023
1 parent 94ee44d commit 1d81717
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 91 deletions.
163 changes: 115 additions & 48 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ function fullName(name: Name | null | undefined): string | null {
}
----

When using the `link:{perfective-common}/src/maybe/index.adoc[Maybe]` monad,
you can write simpler and more readable functions:
**Using the `link:{perfective-common}/src/maybe/index.adoc[Maybe]` monad,
you can write simpler and more readable functions**:

[source,typescript]
----
Expand All @@ -89,7 +89,7 @@ function fullName(name: Name): Maybe<string> {
}
----
<.> `link:{perfective-common}/src/maybe/index.adoc#maybepick[Maybe.pick()]`
provides a strictly-typed "optional chaining" of the `Maybe.value` properties.
provides a strictly typed "optional chaining" of the `Maybe.value` properties.
<.> `link:{perfective-common}/src/maybe/index.adoc#maybeonto[Maybe.onto()]`
(flat) maps a `Maybe.value` to another `Maybe`.
<.> `{perfective-common}/src/maybe/index.adoc#maybeor[Maybe.or()]`
Expand All @@ -107,79 +107,141 @@ the `link:{perfective-common}/src/maybe/index.adoc[Maybe]` type also provides:
`link:{perfective-common}/src/maybe/index.adoc#maybeotherwise[Maybe.otherwise()]`,
and `link:{perfective-common}/src/maybe/index.adoc#maybethrough[Maybe.through()]` methods.

Read more about the `Maybe` monad and other
`link:{perfective-common}/src/maybe/index.adoc[@perfective/common/maybe]` functions in the
link:{perfective-common}/src/maybe/index.adoc[package documentation].


=== Result monad

The `link:{perfective-common}/src/result/index.adoc[@perfective/common/result]` package provides the `Result` monad
(a concrete case of the Either monad).
It allows you to treat errors as part of a function result and chain processing of such results,
instead of throwing and catching errors.
The `link:{perfective-common}/src/result/index.adoc[@perfective/common/result]` package
provides the `Result` monad implementation
(as a concrete case of the Either monad).
It allows developers to increase the reliability of their code by treating errors as valid part of a function output.

A `Result` instance can be either a `Success` or a `Failure`.
If a `Result` is a `Success`, a computation proceeds to the next step.
In case of a `Failure`, all further computations are skipped until the recovery or exit from the computation.

`Result` is similar to `Promise`:
a `Promise` can be resolved or rejected,
while a `Result` is either a `Success` or a `Failure`.
But unlike `Promise`, the `Result` computations are synchronous.
`link:{perfective-common}/src/result/index.adoc[@perfective/common/result]` provides special functions for `Result`/`Promise` interoperability.
The `Result` monad is similar to the `Maybe` monad,
but unlike `Maybe`, a `Result` contains a reason for its `Failure`.

For example, consider you are writing an HTTP endpoint to return an entity stored in a database.
The purpose of endpoint is to map a given unsafe `Input` to an `HttpResponse`.
The `Result` monad is also similar to the `Promise`
(as a `Promise` can either be "resolved" or "rejected").
But, unlike `Promise`, the `Result` chain is synchronous.

.Given functions and types.
For example, consider you have an HTTP endpoint to return user data stored in the database.
The purpose of the endpoint is to map a given (unsafe) user ID input to a `User`.

.Assume you have the following functions
[source,typescript]
----
interface EntityRequest {
entity: string;
id: number;
export interface User {
// User data
}
interface User {
id: number;
username: string;
}
/** Returns `true` if the active user has admin access. */
declare function hasAdminAccess(): boolean;
/**
* @throws {Error} If a given input is empty or is not a number.
*/
declare function numberInput(input: string): number;
/** Builds an SQL query to load a user with a given `id`. */
declare function userByIdQuery(id: number): string;
declare function request(entity: string): (id: number) => EntityRequest;
/** Sends a given `sql` to the database and returns a User. */
declare function userQueryResult(sql: string): Promise<User>;
declare function user(request: EntityRequest): User;
/** Logs a given error */
declare function logError(error: Error): void;
----

Writing an imperative code, you would have:
If you write regular imperative code you may have something like this:

[source,typescript]
----
function userOutput(input: string): User {
let id: number;
/** @throws Error if a given id is invalid. */
function validUserId(id: unknown): number {
if (typeof id !== 'string') {
throw new TypeError('Input must be a string');
}
const userId = decimal(id);
if (userId === null) {
throw new Error('Failed to parse user id');
}
if (!Number.isInteger(userId) || userId <= 0) {
throw new Error('Invalid user id');
}
return userId;
}
async function userResponseById(id: unknown): Promise<User> {
try {
id = numberInput(input);
return userForQuery(
userByIdQuery(
validUserId(id), // <.>
),
);
}
catch {
id = 0;
catch (error: unknown) {
logError(error as Error);
throw error as Error;
}
const userRequest = request('user');
return user(userRequest(id));
}
----
<.> Note that `validUserId()` indicates that it throws an error using a JSDoc.
TypeScript compiler does not check that the code should be wrapped into the `try-catch` block.

Using the `Result` your code would be:
**Using the `Result` monad and functions from the `@perfective/common` subpackages you can write the same code as**:

[source,typescript]
----
import { Result, resultOf } from '@perfective/common/result';
import { isNotNull } from '@perfective/common';
import { typeError } from '@perfective/common/error';
import { naught } from '@perfective/common/function';
import { decimal, isNonNegativeInteger } from '@perfective/common/number';
import { rejected } from '@perfective/common/promise';
import { Result, success } from '@perfective/common/result';
import { isString } from '@perfective/common/string';
function validUserId(id: unknown): Result<number> {
return success(id)
.which(isString, typeError('Input must be a string')) // <.>
.to(decimal)
.which(isNotNull, 'Failed to parse user ID') // <.>
.that(isNonNegativeInteger, 'Invalid user ID'); // <.>
}
function userOutput(id: string): Result<User> {
return resultOf(() => numberInput(id))
.otherwise(0)
.to(request('user'))
.to(user);
async function userResponseById(id: unknown): Promise<User> {
return success(id)
.when(hasAdminAccess, 'Access Denied') // <.>
.onto(validUserId) // <.>
.to(userByIdQuery)
.through(naught, logError) // <.>
.into(userForQuery, rejected); // <.>
}
----

The `Result` link:{perfective-common}/src/result/index.adoc#using-result-with-promise[integrates]
with the `Promise` using the `promisedResult()` and `settledResult()` functions.
<.> `link:{perfective-common}/src/result/index.adoc#resultwhich[Result.which()]`
applies a type guard and narrows the `Result.value` type.
<.> `decimal()` returns `number | null`, so another type guard is required.
<.> `link:{perfective-common}/src/result/index.adoc#resultthat[Result.that()]`
checks if the `Success.value` satisfies a given predicate.
<.> `link:{perfective-common}/src/result/index.adoc#resultwhen[Result.when()]`
checks an external condition.
<.> `link:{perfective-common}/src/result/index.adoc#resultonto[Result.onto()]`
allows a different `Result` object to be returned
(in this case, the `Result` of the `validUserId()` function).
<.> `link:{perfective-common}/src/result/index.adoc#resultthrough[Result.through()]`
runs a given procedure
(a no-op `naught()` function for the `Success`).
<.> `link:{perfective-common}/src/result/index.adoc#resultinto[Result.into()]`
allows the completion (folding) of the `Result` chain computation and switch to a different type.

In addition to the methods used in the example above,
the `Result` monad also provides
`link:{perfective-common}/src/result/index.adoc#resultor[Result.or()]` and
`link:{perfective-common}/src/result/index.adoc#resultotherwise[Result.otherwise()]` methods.

Read more about the `Result` monad and other
`link:{perfective-common}/src/result/index.adoc[@perfective/common/result]` functions in the
link:{perfective-common}/src/result/index.adoc[package documentation].


=== Chained Exceptions
Expand All @@ -193,7 +255,7 @@ Especially in async environments, when most of the stack trace is full of useles
or on the frontend with packed code and renamed functions.

The `link:{perfective-common}/src/error/index.adoc[@perfective/common/error]` package provides the `Exception` class
to make logging and debugging of productions code easier.
to make logging and debugging of production code easier.
It supports three features:

* providing a previous error (allows to stack errors);
Expand Down Expand Up @@ -276,7 +338,7 @@ It will return a similar chain of messages as above,
but each message will also contain a stack trace for each error.

Read more about the functions to handle the built-in JS errors and the `Exception` class in the
`link:{perfective-common}/src/error/index.adoc[@perfective/common/error]` package docs.
`link:{perfective-common}/src/error/index.adoc[@perfective/common/error]` package documentation.


== Packages
Expand Down Expand Up @@ -341,3 +403,8 @@ For example,
a function that declares an argument as _required_ relies on strict TSC `null` checks
and may not additionally check the value for `null`.
====

== Roadmap

The `link:{perfective-common}/ROADMAP.adoc[ROADMAP.adoc]` file describes
how built-in JavaScript objects and methods are covered by the `@perfective/common` package.
Loading

0 comments on commit 1d81717

Please sign in to comment.