1.2.0 • Published 7 months ago

jonads v1.2.0

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

J(avascript M)onads

A set of monadic classes ("Jonads") and utilities for writing clean, programmatically correct code.

List of Jonads

Either

Either left or right. This Jonad represents one of two possible values. It is the base type of other Jonads, such as Result and Option.

There are two concrete types Left and Right that implement the Either interface. Like all other jonads, these classes aren't exposed in this package, but can be created through calls to the related Either const object. See the examples below for how to do this. This jonad is useful for representing a single value that can be of two different variants, but come with type-safety measure through the utility functions defined by the interface.

Examples

Instantiating Either using the provided const object:

const lefty: Either<number, string> = Either.left(3);
const righty: Either<number, string> = Either.right("5");

console.log(lefty.isLeft());
// => true
console.log(right.isRight());
// => true

Working with values using Either.

let numbery: Either<number, string>;

numbery = Either.left(3);
console.log(numbery.isLeft());
// => true
console.log(numbery.leftOr(s => parseInt(s)));
// => 3

// ...

numbery = Either.right("5");
console.log(numbery.isRight());
// => true
console.log(numbery.leftOr(s => parseInt(s)));
// => 5

Result

Result represents a value or an error that can be associated with an attempt to resolve the value. This is a subtype of Either, and also has two implicit concrete classes: Ok representing any value and Err representing an error that occurred during the attempt to resolve the value.

Examples

Instantiating Result using the provided const object:

const okish: Result<string, Error> = Result.ok("I'm ok!");
const errish: Result<string, Error> = Result.err(new Error("I'm not ok!"));

console.log(okish.isOk());
// => true
console.log(errish.isErr());
// => true

Working with values using Result.

// async function safeFetch(url: string): Promise<Result<Response, HttpError>>

// When the fetch is successful...

const result: Result<Response, HttpError> = await safeFetch("https://example.com/api/data");
console.log(result.isOk());
// => true

const body: Result<object, HttpError> = await result.mapAsync(async response => await response.json());
console.log(body.valueOr({}));
// => { data: "..." }

// ...when the fetch is not successful...

const result: Result<Response, HttpError> = await safeFetch("https://example.com/api/data");
console.log(result.isErr());

const body: Result<object, HttpError> = await result.mapAsync(async response => await response.json());
console.log(body.valueOr({}));
// => {}

Jonads' methods are also paired with a higher order function for working in more complex situations, such as a list of results.

const results: Result<number, Error>[] = [Result.ok(1), Result.ok(2), Result.error("something happened!")];
const newResults = results
    .filter(Result.isOk())
    .map(Result.map(result => result * 2))

console.log(newResults)
// => [Ok(2), Ok(4)]

Option

Option represents a value that may or may not exist. This is a subtype of Either, and also has two implicit concrete classes: Some representing the existence of a value and None representing the absence of a value.

Examples

Instantiating Option using the provided const object:

const something: Option<string> = Option.from("I'm ok!");
const nothing: Option<string> = Option.none();
// or, if you have a potentially nullable/undefined value...
const maybe: Option<string> = Option.from(getNullableValue());

console.log(something.isSome());
// => true
console.log(nothing.isNone());
// => true

Working with values using Option.

const stack: string[] = ["a"];

let nextValue: Option<string> = Option.from(stack.pop());
console.log(nextValue.isSome());
// => true

nextValue = Option.from(stack.pop());
console.log(nextValue.isNone());
// => true

Working with Jonads

Jonads are useful as they exist in their concrete class forms, but they can be tedious to work with if a set of utility functions aren't supplied.

"Do-notation" and Results

There's a concept in Haskell called "do-notation". Effectively, this notation allows Haskell to extract the side-effects from function/service calls (for us, Err in a Result) and focus primarily on the happy-path values.

To recreate this notation, the doing function can be used to run a sequence of services (fallible functions). By calling this function at the top of any function, and passing the actual function logic to it through a callback, doing can capture any thrown errors into Err values and allow you, the developer, to extract the side-effects away from the function's logic via a call to the provided bind function.

Examples

Let's say you want to define a new service to get the name of a workspace a user is associated with.

You already have two services defined to get the User and Workspace records by ID. These database calls can fail for numerous reasons, but the new service you're trying to define has no concern with handling errors and wants to propagate them to the caller.

// Existing services:
//   getUser(id: number): Result<User, GetUserError>
//   getWorkspace(id: number): Result<Workspace, GetWorkspaceError>

// getUserWorkspaceName(userId: number): Result<string, DatabaseError>
const getUserWorkspaceName = (userId) => doing(bind => {
    const user = bind(getUser(userId));
    const workspace = bind(getWorkspace(user.workspaceId));
    return workspace.name
});

Although doing captures any thrown exceptions in a Result as Errs, there may be simpler cases where you would only want to capture specific (or any) execption from a single function call that may not return a result. You can do this using trying and tryCatching.

const result: Result<Response, Error> = await tryingAsync(async () => await fetch("https://example.com"));
console.log(result.isOk());
// => true

result.matchAsync(
    res => console.log(await res.json()),
    err => console.error(err),
);
// => { data: "..." }
1.2.0

7 months ago

1.1.4

7 months ago

1.1.3

8 months ago

1.1.2

8 months ago

1.1.1

8 months ago

1.1.0

8 months ago

1.0.2

8 months ago

1.0.1

8 months ago

1.0.0

8 months ago