diff --git a/README.md b/README.md index b7d7aa8..255cdc2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ This is a set of implementations of monads in TypeScript with OOP perspective. * [Mapping over an Either](#mapping-over-an-either) * [Using `flatMap` and `flatMapLeft`](#using-flatmap-and-flatmapleft) * [Using `map` and `mapLeft`](#using-map-and-mapleft) - * [Matching an Either](#matching-an-either) + * [Recovering from a Left value](#recovering-from-a-left-value) + * [Running side effects](#running-side-effects) + * [Folding an Either](#folding-an-either) * [checking if an Either is Right or Left](#checking-if-an-either-is-right-or-left) * [Chaining operations](#chaining-operations) * [Handling errors](#handling-errors) @@ -24,15 +26,18 @@ This is a set of implementations of monads in TypeScript with OOP perspective. * [Mapping over an Option](#mapping-over-an-option) * [Using `flatMap`](#using-flatmap) * [Using `map`](#using-map) - * [Matching an Option](#matching-an-option) + * [Running side effects](#running-side-effects-1) + * [Folding an Option](#folding-an-option) * [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) + * [Running side effects](#running-side-effects-2) + * [Retrieving the value](#retrieving-the-value) + * [Folding a Try](#folding-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) @@ -105,7 +110,9 @@ transform the value inside a `Left`. ##### Using `flatMap` and `flatMapLeft` ```typescript -import { Either } from '@leanmind/monads';m +import { Either } from '@leanmind/monads'; + +m const right = Either.right(42).flatMap(x => Either.right(x + 1)); // Right(43) const left = Either.left('Error').flatMapLeft(err => Either.left(`New ${err}`)); // Left('New Error') @@ -120,22 +127,62 @@ const right = Either.right(42).map(x => x + 1); // Right(43) const left = Either.left('Error').mapLeft(err => `New ${err}`); // Left('New Error') ``` -#### Matching an Either +#### Recovering from a Left value + +You can use the `recover` method to recover from a `Left` value and transform it into a `Right`. + +```typescript +import { Either } from '@leanmind/monads'; + +const recoverIfEven = (x: number) => { + if (x % 2 === 0) { + return Either.right('Even'); + } + return Either.left('Not even'); +}; + +const right = Either.right('irrelevant').recover(recoverIfEven); // Right('irrelevant') + +const leftEven = Either.left(42).recover(recoverIfEven); // Right('Even') +const leftOdd = Either.left(43).recover(recoverIfEven); // Left('Not even') +``` + +#### Running side effects + +You can use the `onRight` method to run side effects on the value inside a `Right`. + +```typescript +import { Either } from '@leanmind/monads'; + +const right = Either.right(42).onRight(x => console.log(x)); // 42 +const left = Either.left('Error').onRight(x => console.log(x)); // No execution +``` + +Or you can use the `onLeft` method to run side effects on the value inside a `Left`. + +```typescript +import { Either } from '@leanmind/monads'; + +const right = Either.right(42).onLeft(err => console.log(err)); // No execution +const left = Either.left('Error').onLeft(err => console.log(err)); // 'Error' +``` + +#### Folding an Either -You can use the `match` method to handle both `Right` and `Left` cases and unwrap the result. +You can use the `fold` method to handle both `Right` and `Left` cases and unwrap the result. ```typescript import { Either } from '@leanmind/monads'; -const sucess = Either.right(42).match( - err => `Error: ${err}`, - x => (x + 1).toString() -); // '43' +const success = Either.right(42).fold({ + ifRight: x => `${x + 1}`, + ifLeft: err => `Error: ${err}`, +}); // '43' -const error = Either.left('Error').match( - err => `Error: ${err}`, - x => (x + 1).toString(), -); // 'Error: Error' +const error = Either.left('Error').fold({ + ifRight: x => `${x + 1}`, + ifLeft: err => `Error: ${err}`, +}); // 'Error: Error' ``` #### checking if an Either is Right or Left @@ -168,10 +215,10 @@ import { Either } from '@leanmind/monads'; const result = Either.right(42) .map(x => x + 1) .map(x => x * 2) - .match( - err => `Error: ${err}`, - x => x - ); + .fold({ + ifRight: x => x.toString(), + ifLeft: err => `Error: ${err}`, + }) console.log(result); // 86 ``` @@ -192,16 +239,16 @@ function divide(a: number, b: number): Either { const result = divide(10, 2) .map(x => x * 2) - .match( - err => `Error: ${err}`, - value => `Result: ${value}` - ); + .fold({ + ifRight: x => `Result: ${x}`, + ifLeft: err => `Error: ${err}`, + }); console.log(result); // 'Result: 10' ``` In this example, the divide function returns an `Either` that represents the result of the division or an error if the -division is by zero. The result is then transformed and matched to produce a final `string`. +division is by zero. The result is then transformed and folded to produce a final `string`. ## Option Monad @@ -244,7 +291,9 @@ none.getOrElse(0); // 0 You can use the `filter` method to keep the `Some` value if it satisfies a predicate. ```typescript -import { Option } from '@leanmind/monads';m +import { Option } from '@leanmind/monads'; + +m const some = Option.of(42).filter(x => x > 40); // Some(42) const none = Option.of(42).filter(x => x > 50); // None @@ -257,7 +306,9 @@ You can use the `flatMap` or `map` method to transform the `Some` value. ##### Using `flatMap` ```typescript -import { Option } from '@leanmind/monads';m +import { Option } from '@leanmind/monads'; + +m const some = Option.of(42).flatMap(x => Option.of(x + 1)); // Some(43) const none = Option.of(null).flatMap(x => Option.of(x + 1)); // None @@ -272,22 +323,42 @@ const some = Option.of(42).map(x => x + 1); // Some(43) const none = Option.of(null).map(x => x + 1); // None ``` -#### Matching an Option +#### Running side effects -You can use the `match` method to handle both `Some` and `None` cases and unwrap the result. +You can use the `onSome` method to run side effects on the value inside a `Some`. ```typescript import { Option } from '@leanmind/monads'; -const some = Option.of(42).match( - x => x + 1, - () => 'No value' -); // 43 +const some = Option.some(42).onSome(x => console.log(x)); // 42 +const none = Option.none().onSome(x => console.log(x)); // No execution +``` + +Or you can use the `onNone` method to run side effects on the value inside a `None`. -const none = Option.of(null).match( - x => x + 1, - () => 'No value' -); // 'No value' +```typescript +import { Option } from '@leanmind/monads'; + +const some = Option.some(42).onNone(_ => console.log('Empty value')); // No execution +const none = Option.none().onNone(_ => console.log('Empty value')); // 'Empty value' +``` + +#### Folding an Option + +You can use the `fold` method to handle both `Some` and `None` cases and unwrap the result. + +```typescript +import { Option } from '@leanmind/monads'; + +const some = Option.of(42).fold({ + ifSome: x => `${x + 1}`, + ifNone: () => 'No value', +}); // '43' + +const none = Option.of(null).fold({ + ifSome: x => `${x + 1}`, + ifNone: () => 'No value', +}); // 'No value' ``` #### Checking if an Option is Some or None @@ -312,7 +383,17 @@ The `Try` monad represents a computation that may fail. ### Usage -You can create a `Try` using the static method `Try.execute`. +you can create a `Try` using the static method `Try.success` or `Try.failure`. + +```typescript +import { Try } from '@leanmind/monads'; + +const success = Try.success(42); // Success(42) + +const failure = Try.failure(new Error('Error')); // Failure(Error('Error')) +``` + +Also, you can create a `Try` using the static method `Try.execute` from a function that may throw an exception. ```typescript import { Try } from '@leanmind/monads'; @@ -324,47 +405,93 @@ const failure = Try.execute(() => { }); // Failure(Error('Error')) ``` -### Using `map` +#### Using `map` You can use the `map` method to transform the value inside a `Success`. ```typescript -import { Try } from '@leanmind/monads';m +import { Try } from '@leanmind/monads'; -const success = Try.execute(() => 42).map(x => x + 1); // Success(43) +m + +const success = Try.success(42).map(x => x + 1); // Success(43) ``` -### Using `flatMap` +#### Using `flatMap` You can use the `flatMap` method to transform the value inside a `Success` with a fallible closure. ```typescript import { Try } from '@leanmind/monads'; -const success = Try.execute(() => 42).flatMap(x => Try.execute(() => x + 1)); // Success(43) +const success = Try.success(42).flatMap(x => Try.success(x + 1)); // Success(43) ``` -### Matching a Try +#### Running side effects -You can use the `match` method to handle both `Success` and `Failure` cases and unwrap the result. +You can use the `onSuccess` method to run side effects on the value inside a `Success`. ```typescript import { Try } from '@leanmind/monads'; -const success = Try.execute(() => 42).match( - err => `Error: ${err}`, - x => x + 1 -); // 43 +const succcess = Try.succcess(42).onSuccess(x => console.log(x)); // 42 +const failure = Try.failure('Error').onSuccess(x => console.log(x)); // No execution +``` -const failure = Try.execute(() => { - throw new Error('Error'); -}).match( - err => `Error: ${err}`, - x => x + 1 -); // 'Error: Error' +Or you can use the `onFailure` method to run side effects on the value inside a `Failure`. + +```typescript +import { Try } from '@leanmind/monads'; + +const succcess = Try.succcess(42).onFailure(err => console.log(err)); // No execution +const failure = Try.failure(new Error('Error')).onFailure(err => console.log(err)); // Error('Error') ``` -### Handling errors in Infrastructure code +#### Retrieving the value + +You can use the `getOrElse` method to retrieve the value of a `Success` or provide a default value if it is `Failure`. + +```typescript +import { Try } from '@leanmind/monads'; + +const success = Try.success(42); +const value = success.getOrElse(0); // 42 + +const failure = Try.failure(new Error('Error')); +const otherValue = failure.getOrElse(0); // 0 +``` + +Also, you can use the `getOrThrow` method to retrieve the value of a `Success` or throw the error if it is `Failure`. + +```typescript +import { Try } from '@leanmind/monads'; + +const success = Try.success(42); +const value = success.getOrThrow(); // 42 + +const failure = Try.failure(new Error('Error')); +const otherValue = failure.getOrThrow(); // throws Error('Error') +``` + +#### Folding a Try + +You can use the `fold` method to handle both `Success` and `Failure` cases and unwrap the result. + +```typescript +import { Try } from '@leanmind/monads'; + +const success = Try.success(42).fold({ + ifSuccess: x => `${x + 1}`, + ifFailure: err => `Error: ${err}`, +}); // '43' + +const failure = Try.failure(new Error('an error')).fold({ + ifSuccess: x => `${x + 1}`, + ifFailure: err => `Error: ${err}`, +}); // 'Error: an error' +``` + +#### Handling errors in Infrastructure code Normally, Try is used to handle `Exceptions` that are raise by third party libraries @@ -374,15 +501,15 @@ import { Try } from '@leanmind/monads'; const result = Try.execute(() => { // Some API of a library that may throw an exception return 42; -}).match( - err => `Error: ${err}`, - x => x + 1 -); +}).fold({ + ifSuccess: x => `${x + 1}`, + ifFailure: err => `Error: ${err.message}`, +}) console.log(result); // 43 ``` -### Checking if a Try is Success or Failure +#### Checking if a Try is Success or Failure If needed, you can check explicitly if a `Try` is `Success` or `Failure` using the `isSuccess` and `isFailure` methods. @@ -479,6 +606,7 @@ So, you can operate as pure functions until you call the `runUnsafe` method. ### Usage #### Creating an IO + You can create an `IO` using the static method `IO.of`. ```typescript diff --git a/package.json b/package.json index 8c3e05a..fa2627f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@leanmind/monads", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", - "types": "dist/index.d.ts", + "types": "dist/types/index.d.ts", "version": "0.0.0-semantically-released", "description": "A collection of monads implemented in TypeScript using object-oriented programming.", "keywords": [ @@ -34,7 +34,7 @@ "author": "Lean Mind", "contributors": [ { - "name": "Mario Pinto Miranda", + "name": "Mario Sebastián Pinto Miranda", "email": "mario.pinto.miranda@gmail.com", "url": "https://mario-pinto-miranda.dev" }, diff --git a/src/either/either.test.ts b/src/either/either.test.ts index c4fdb45..d230004 100644 --- a/src/either/either.test.ts +++ b/src/either/either.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { Either } from './either'; import { Option } from '../option'; @@ -17,20 +17,20 @@ describe('Either monad', () => { }); it.each([ - { typeMatchable: 'Some', eitherType: 'Right', matchable: Option.of(2), expected: Either.right(2) }, + { typeFoldable: 'Some', eitherType: 'Right', foldable: Option.of(2), expected: Either.right(2) }, { - typeMatchable: 'None', + typeFoldable: 'None', eitherType: 'Left', - matchable: Option.of(undefined), + foldable: Option.of(undefined), expected: Either.left(undefined), }, - ])('$eitherType should be created from $typeMatchable', ({ matchable, expected }) => { - expect(Either.from(matchable)).toEqual(expected); + ])('$eitherType should be created from $typeFoldable', ({ foldable, expected }) => { + expect(Either.from(foldable)).toEqual(expected); }); it.each([ - { type: 'Right', either: Either.right(2), closure: (x: number) => x, expected: Either.right(2) }, - { type: 'Left', either: Either.left(2), closure: (x: number) => x * 2, expected: Either.left(4) }, + { type: 'Right', either: Either.right(2), closure: (x: number) => x, expected: Either.right(2) }, + { type: 'Left', either: Either.left(2), closure: (x: number) => x * 2, expected: Either.left(4) }, ])('$type should handle mapLeft operation correctly', ({ either, closure, expected }) => { expect(either.mapLeft(closure)).toEqual(expected); }); @@ -44,33 +44,6 @@ describe('Either monad', () => { expect(either.flatMapLeft(closure)).toEqual(expected); }); - it.each([ - { - type: 'Right', - either: Either.right(2), - fr: (x: number) => x, - fl: (x: number) => x.toString(), - expected: 2, - }, - ])( - 'Either $type can handle closures to unwrap distinct types of results by algebraic types', - ({ either, expected, fr, fl }) => { - expect(either.match(fr, fl)).toEqual(expected); - } - ); - - it.each([ - { - type: 'Left', - either: Either.left('Some Error'), - fr: (x: string) => x, - fl: (error: string) => `Error: ${error}`, - expected: 'Error: Some Error', - }, - ])('Either $type can handle closures to unwrap distinct types of results', ({ either, expected, fr, fl }) => { - expect(either.match(fr, fl)).toEqual(expected); - }); - it.each([ { type: 'Right', either: Either.right(2), expected: false }, { type: 'Left', either: Either.left('Error'), expected: true }, @@ -84,4 +57,34 @@ describe('Either monad', () => { ])('$type should handle isRight operation correctly', ({ either, expected }) => { expect(either.isRight()).toEqual(expected); }); + + it('should execute an action if the Either is a Right', () => { + const action = vi.fn(); + Either.right(2).onRight(action); + expect(action).toHaveBeenCalledWith(2); + + const nonCallableAction = vi.fn(); + Either.left(2).onRight(nonCallableAction); + expect(nonCallableAction).not.toHaveBeenCalled(); + }); + + it('should execute an action if the Either is a Left', () => { + const action = vi.fn(); + Either.left(2).onLeft(action); + expect(action).toHaveBeenCalledWith(2); + + const nonCallableAction = vi.fn(); + Either.right(2).onLeft(nonCallableAction); + expect(nonCallableAction).not.toHaveBeenCalled(); + }); + + it('should recover from a Left value', () => { + const recoverIfEven = (x: number): Either => + x % 2 === 0 ? Either.right('Even number') : Either.left('Odd number'); + const leftEven = Either.left(2); + const leftOdd = Either.left(3); + + expect(leftEven.recover(recoverIfEven)).toEqual(Either.right('Even number')); + expect(leftOdd.recover(recoverIfEven)).toEqual(Either.left('Odd number')); + }); }); diff --git a/src/either/either.ts b/src/either/either.ts index bcb6455..f750f36 100644 --- a/src/either/either.ts +++ b/src/either/either.ts @@ -1,56 +1,65 @@ import { Monad } from '../monad'; -import { Matchable } from '../match'; import { Future } from '../future'; import { Futurizable } from '../futurizable'; +import { Foldable, Folding } from '../fold'; + +type FoldingEither = Folding<'Either', R, L, T>; /** * Abstract class representing a value that can be one of two possible types. * @template L The type of the left value. * @template R The type of the right value. */ -abstract class Either implements Monad, Matchable, Futurizable { +abstract class Either implements Monad, Futurizable, Foldable { /** * Creates a `Right` instance. - * @template T The type of the right value. - * @param {T} value The right value. - * @returns {Either} A `Right` instance containing the value. + * @template L The type of the left value. + * @template R The type of the right value. + * @param {R} value The right value. + * @returns {Either} A `Right` instance containing the value. * @example * const right = Either.right(5); - * right.match(console.log, error => console.error(error.message)); // 5 + * right.fold({ ifRight: console.log, ifLeft: error => console.error(error.message) }); // 5 */ - static right(value: T): Either { + static right(value: R): Either { return new Right(value); } /** * Creates a `Left` instance. - * @template T The type of the left value. - * @param {T} value The left value. - * @returns {Either} A `Left` instance containing the value. + * @template L The type of the left value. + * @template R The type of the right value. + * @param {L} value The left value. + * @returns {Either} A `Left` instance containing the value. * @example * const left = Either.left('error'); - * left.match(console.log, error => console.error(error.message)); // 'error' + * left.fold({ ifRight: console.log, ifLeft: error => console.error(error.message) }); // 'error' */ - static left(value: T): Either { + static left(value: L): Either { return new Left(value); } /** - * Creates an `Either` instance from a `Matchable` instance. + * Creates an `Either` instance from a `Foldable` instance. * @template L The type of the left value. * @template R The type of the right value. - * @param {Matchable} matchable The matchable instance. - * @returns {Either} A `Right` instance if the matchable contains a right value, otherwise a `Left` instance. + * @param {Foldable} foldable The foldable instance. + * @returns {Either} A `Right` instance if the foldable contains a right value, otherwise a `Left` instance. * @example * const option = Option.of(5); * const either = Either.from(option); - * either.match(console.log, error => console.error(error.message)); // 5 + * either.fold({ ifRight: console.log, ifLeft: error => console.error(error.message) }); // 5 */ - static from(matchable: Matchable): Either { - return matchable.match>( - (value: R) => Either.right(value), - (value: L) => Either.left(value) - ); + static from(foldable: Foldable): Either { + const folding = { + ifRight: (value: R): Either => Either.right(value), + ifLeft: (value: L) => Either.left(value), + ifSuccess: (value: R) => Either.right(value), + ifFailure: (value: L) => Either.left(value), + ifSome: (value: R) => Either.right(value), + ifNone: (value: L) => Either.left(value), + }; + return foldable.fold>(folding); } /** @@ -60,7 +69,7 @@ abstract class Either implements Monad, Matchable, Futurizable * @returns {Either} A `Right` instance if the function executes without error, otherwise a `Left` instance. * @example * const result = Either.catch(() => JSON.parse('invalid json')); - * result.match(console.log, error => console.error(error.message)); // 'SyntaxError: Unexpected token i in JSON at position 0' + * result.fold({ ifRight: console.log, ifLeft: error => console.error(error.message) }); // Error: Unexpected token i in JSON at position 0 */ static catch(execute: () => T): Either { try { @@ -72,66 +81,108 @@ abstract class Either implements Monad, Matchable, Futurizable /** * Transforms the right value contained in this `Either` instance. + * @template R The type of the right value. * @template T The type of the transformed value. * @param {(r: R) => T} transform The transformation function. * @returns {Either} A new `Either` instance containing the transformed value. * @example * const result = Either.right(5).map(value => value * 2); - * result.match(console.log, console.error); // 10 + * result.fold({ ifRight: console.log, ifLeft: error => console.error(error.message) }); // 10 */ abstract map(transform: (r: R) => T): Either; /** * Transforms the left value contained in this `Either` instance. + * @template L The type of the left value. * @template T The type of the transformed value. + * @template R The type of the right value. * @param {(l: L) => T} transform The transformation function. * @returns {Either} A new `Either` instance containing the transformed value. * @example * const result = Either.left('error').mapLeft(value => `Error: ${value}`); - * result.match(console.log, console.error); // 'Error: error' + * result.fold({ ifRight: console.log, ifLeft: error => console.error(error.message) }); // Error: error */ abstract mapLeft(transform: (l: L) => T): Either; + abstract flatMap(transform: (r: R) => Either): Either; /** * Transforms the right value contained in this `Either` instance into another `Either` instance. + * @template R The type of the right value. * @template T The type of the transformed value. * @param {(r: R) => Either} transform The transformation function. * @returns {Either} The result of the transformation function. * @example * const result = Either.right(5).flatMap(value => Either.right(value * 2)); - * result.match(console.log, console.error); // 10 + * result.fold({ ifRight: console.log, ifLeft: error => console.error(error.message) }); // 10 */ - abstract flatMap(transform: (r: R) => Either): Either; /** * Transforms the left value contained in this `Either` instance into another `Either` instance. + * @template L The type of the left value. * @template T The type of the transformed value. + * @template R The type of the right value. * @param {(l: L) => Either} transform The transformation function. * @returns {Either} The result of the transformation function. * @example * const result = Either.left('error').flatMapLeft(value => Either.left(`Error: ${value}`)); - * result.match(console.log, console.error); // Error: error + * result.fold({ ifRight: console.log, ifLeft: error => console.error(error.message) }); // Error: Error: error */ abstract flatMapLeft(transform: (l: L) => Either): Either; + /** + * Recovers from an error by transforming the left value into a right value. + * @template L The type of the left value. + * @template R The type of the right value. + * @template T The type of the new left value. + * @param {(l: L) => Either} transform The transformation function. + * @returns {Either} A new `Either` instance containing the transformed value. + * @example + * const result = Either.left('error').recover(value => Either.right(`Recovered: ${value}`)); + * result.fold({ ifRight: console.log, ifLeft: error => console.error(error.message) }); // Recovered: error + */ + abstract recover(transform: (l: L) => Either): Either; + + /** + * Executes an action if this is a `Right` instance. + * @template L The type of the left value. + * @template R The type of the right value. + * @param {(r: R) => void} action The action to execute if this is a `Right` instance. + * @returns {Either} The current `Either` instance. + * @example + * const result = Either.right(5).onRight(value => console.log(value)); // prints 5 + */ + abstract onRight(action: (r: R) => void): Either; + + /** + * Executes an action if this is a `Left` instance. + * @template L The type of the left value. + * @template R The type of the right value. + * @param {(l: L) => void} action The action to execute if this is a `Left` instance. + * @returns {Either} The current `Either` instance. + * @example + * const result = Either.left('error').onLeft(value => console.error(value)); // prints 'error' + */ + abstract onLeft(action: (l: L) => void): Either; + /** * Unwraps the value contained in this `Either` instance by applying the appropriate handler for both Left and Right cases. + * @template R The type of the right value. + * @template L The type of the left value. * @template T The type of the result. - * @param {(r: R) => T} ifRight The function to call if this is a `Right` instance. - * @param {(l: L) => T} ifLeft The function to call if this is a `Left` instance. - * @returns {T} The result of the matching function. + * @param {FoldingEither} folding The folding object containing the functions to call for each case. + * @returns {T} The result of the folding function. * @example * const result = Either.right(5); - * result.match(console.log, console.error); // 5 + * result.fold({ ifRight: console.log, ifLeft: console.error }); // 5 */ - abstract match(ifRight: (r: R) => T, ifLeft: (l: L) => T): T; + abstract fold(folding: FoldingEither): T; /** * Checks if this is a `Left` instance. * @returns {boolean} `true` if this is a `Left` instance, otherwise `false`. * @example * const result = Either.left('error'); - * console.log(result.isLeft()); // true + * result.isLeft(); // true */ abstract isLeft(): this is Left; @@ -140,7 +191,7 @@ abstract class Either implements Monad, Matchable, Futurizable * @returns {boolean} `true` if this is a `Right` instance, otherwise `false`. * @example * const result = Either.right(5); - * console.log(result.isRight()); // true + * result.isRight(); // true */ abstract isRight(): this is Right; @@ -173,7 +224,7 @@ class Left extends Either { super(); } - map(_: (r: never) => never): Either { + map(_: (r: R) => T): Either { return new Left(this.value); } @@ -181,7 +232,7 @@ class Left extends Either { return new Left(transform(this.value)); } - flatMap(_: (r: never) => Either): Either { + flatMap(_: (r: R) => Either): Either { return new Left(this.value); } @@ -189,8 +240,21 @@ class Left extends Either { return transform(this.value); } - match(_: (_: never) => never, ifLeft: (l: L) => T): T { - return ifLeft(this.value); + recover(transform: (l: L) => Either): Either { + return transform(this.value); + } + + onRight(_: (r: R) => void): Either { + return this; + } + + onLeft(action: (l: L) => void): Either { + action(this.value); + return this; + } + + fold(folding: FoldingEither): T { + return folding.ifLeft(this.value); } isLeft(): this is Left { @@ -224,7 +288,7 @@ class Right extends Either { return new Right(transform(this.value)); } - mapLeft(_: (l: L) => never): Either { + mapLeft(_: (l: L) => T): Either { return new Right(this.value); } @@ -232,12 +296,25 @@ class Right extends Either { return transform(this.value); } - flatMapLeft(_: (l: never) => Either): Either { + flatMapLeft(_: (l: L) => Either): Either { return new Right(this.value); } - match(ifRight: (r: R) => T, _: (_: never) => never): T { - return ifRight(this.value); + recover(_: (l: L) => Either): Either { + return new Right(this.value); + } + + onRight(action: (r: R) => void): Either { + action(this.value); + return this; + } + + onLeft(_: (l: L) => void): Either { + return this; + } + + fold(folding: FoldingEither): T { + return folding.ifRight(this.value); } isLeft(): this is Left { diff --git a/src/fold/foldable.test.ts b/src/fold/foldable.test.ts new file mode 100644 index 0000000..fcf257d --- /dev/null +++ b/src/fold/foldable.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest'; +import { Try } from '../try'; +import { Either } from '../either'; +import { Option } from '../option'; + +const testsFoldableCases = [ + { + type: 'Either Right', + foldable: Either.right(2), + folding: { + ifRight: (x: number) => x * 2, + ifLeft: (_: number) => 2, + }, + expected: 4, + }, + { + type: 'Either Left', + foldable: Either.left(2), + folding: { + ifRight: (_: number) => 2, + ifLeft: (x: number) => x * 2, + }, + expected: 4, + }, + { + type: 'Try Success', + foldable: Try.success(2), + folding: { + ifSuccess: (x: number) => x * 2, + ifFailure: (_: Error) => 2, + }, + expected: 4, + }, + { + type: 'Try Failure', + foldable: Try.failure(new Error('fail')), + folding: { + ifSuccess: (_: number) => 2, + ifFailure: (e: Error) => e.message, + }, + expected: 'fail', + }, + { + type: 'Option Some', + foldable: Option.of(2), + folding: { + ifSome: (x: number) => x * 2, + ifNone: () => 2, + }, + expected: 4, + }, + { + type: 'Option None', + foldable: Option.of(undefined), + folding: { + ifSome: (_: number) => 2, + ifNone: (x: undefined) => x, + }, + expected: undefined, + }, +]; + +describe('Foldable', () => { + it.each(testsFoldableCases)( + '$type foldable should handle fold operation correctly', + ({ foldable, folding, expected }) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(foldable.fold(folding)).toEqual(expected); + } + ); +}); diff --git a/src/fold/foldable.ts b/src/fold/foldable.ts new file mode 100644 index 0000000..c2bb502 --- /dev/null +++ b/src/fold/foldable.ts @@ -0,0 +1,21 @@ +type Kind = 'Option' | 'Either' | 'Try'; + +type OptionFoldingKeys = ['ifSome', 'ifNone']; +type EitherFoldingKeys = ['ifRight', 'ifLeft']; +type TryFoldingKeys = ['ifSuccess', 'ifFailure']; + +export type Pair = K extends 'Option' + ? OptionFoldingKeys + : K extends 'Either' + ? EitherFoldingKeys + : K extends 'Try' + ? TryFoldingKeys + : never; + +export type Folding = { + [success in Pair[0]]: (value: H) => T; +} & { [failure in Pair[1]]: (value: W) => T }; + +export interface Foldable { + fold(folding: Folding): R; +} diff --git a/src/fold/index.ts b/src/fold/index.ts new file mode 100644 index 0000000..d9f8955 --- /dev/null +++ b/src/fold/index.ts @@ -0,0 +1 @@ +export type { Folding, Foldable } from './foldable'; diff --git a/src/match/index.ts b/src/match/index.ts deleted file mode 100644 index 26ac686..0000000 --- a/src/match/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { Matchable } from './matchable'; diff --git a/src/match/matchable.test.ts b/src/match/matchable.test.ts deleted file mode 100644 index 4bf77fa..0000000 --- a/src/match/matchable.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { Try } from '../try'; -import { Either } from '../either'; -import { Option } from '../option'; - -const testsMatchableCases = [ - { - type: 'Either Right', - matchable: Either.right(2), - closureF: (x: number) => x * 2, - closureG: () => 2, - expected: 4, - }, - { - type: 'Either Left', - matchable: Either.left(2), - closureF: () => 2, - closureG: (x: number) => x * 2, - expected: 4, - }, - { - type: 'Try Success', - matchable: Try.execute(() => 2), - closureF: (x: number) => x * 2, - closureG: () => 2, - expected: 4, - }, - { - type: 'Try Failure', - matchable: Try.execute(() => { - throw new Error(); - }), - closureF: () => 2, - closureG: (x: Error) => x, - expected: new Error(), - }, - { - type: 'Option Some', - matchable: Option.of(2), - closureF: (x: number) => x * 2, - closureG: () => 2, - expected: 4, - }, - { - type: 'Option None', - matchable: Option.of(undefined), - closureF: () => 2, - closureG: (x: undefined) => x, - expected: undefined, - }, -]; - -describe('Matchable', () => { - it.each(testsMatchableCases)( - '$type matchable should handle match operation correctly', - ({ matchable, expected, closureF, closureG }) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - // the closures can be different for each monad but in the end the behavior is the same - expect(matchable.match(closureF, closureG)).toEqual(expected); - } - ); -}); diff --git a/src/match/matchable.ts b/src/match/matchable.ts deleted file mode 100644 index 0774dc3..0000000 --- a/src/match/matchable.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Matchable { - match(f: (value: T) => S, g: (other: U) => S): S; -} diff --git a/src/option/option.test.ts b/src/option/option.test.ts index 0899fda..f09c093 100644 --- a/src/option/option.test.ts +++ b/src/option/option.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { None, Option, Some } from './option'; import { Either } from '../either'; @@ -11,15 +11,15 @@ describe('Option monad', () => { }); it.each([ - { typeMatchable: 'Right', optionType: 'Some', matchable: Either.right(2), expected: Option.of(2) }, + { typeFoldable: 'Right', optionType: 'Some', foldable: Either.right(2), expected: Option.of(2) }, { - typeMatchable: 'Left', + typeFoldable: 'Left', optionType: 'None', - matchable: Either.left(12), + foldable: Either.left(12), expected: Option.of(undefined), }, - ])('$optionType should be created from $typeMatchable', ({ matchable, expected }) => { - expect(Option.from(matchable)).toEqual(expected); + ])('$optionType should be created from $typeFoldable', ({ foldable, expected }) => { + expect(Option.from(foldable)).toEqual(expected); }); it.each([ @@ -57,4 +57,24 @@ describe('Option monad', () => { it('should create a None', () => { expect(Option.none()).toEqual(new None()); }); + + it('should execute an action if is a Some', () => { + const action = vi.fn(); + Option.some(2).onSome(action); + expect(action).toHaveBeenCalledWith(2); + + const nonCallableAction = vi.fn(); + Option.none().onSome(nonCallableAction); + expect(nonCallableAction).not.toHaveBeenCalled(); + }); + + it('should execute an action if is a None', () => { + const action = vi.fn(); + Option.none().onNone(action); + expect(action).toHaveBeenCalled(); + + const nonCallableAction = vi.fn(); + Option.some(2).onNone(nonCallableAction); + expect(nonCallableAction).not.toHaveBeenCalled(); + }); }); diff --git a/src/option/option.ts b/src/option/option.ts index e14d027..3768115 100644 --- a/src/option/option.ts +++ b/src/option/option.ts @@ -1,14 +1,16 @@ -import { Present, Nullable } from '../types'; +import { Nullable, Present } from '../types'; import { Monad } from '../monad'; -import { Matchable } from '../match'; import { Futurizable } from '../futurizable'; import { Future } from '../future'; +import { Foldable, Folding } from '../fold'; + +type FoldingOption = Folding<'Option', T, undefined, U>; /** * Abstract class representing an optional value. * @template T The type of the value. */ -abstract class Option implements Monad, Matchable, Futurizable { +abstract class Option implements Monad, Futurizable, Foldable { /** * Creates an `Option` instance from a nullable value. * @template T The type of the value. @@ -16,10 +18,10 @@ abstract class Option implements Monad, Matchable, Futurizab * @returns {Option} A `Some` instance if the value is not null or undefined, otherwise a `None` instance. * @example * const some = Option.of(5); - * some.match(console.log, () => console.log('none')); // 5 + * some.fold({ ifSome: console.log, ifNone: () => console.log('none') }); // 5 * * const none = Option.of(null); - * none.match(console.log, () => console.log('none')); // none + * none.fold({ ifSome: console.log, ifNone: () => console.log('none') }); // none */ static of(value: Nullable): Option { if (value == null) { @@ -35,7 +37,7 @@ abstract class Option implements Monad, Matchable, Futurizab * @returns {Some} A `Some` instance of the value * @example * const some = Option.some(5); - * some.match(console.log, () => console.log('none')); // 5 + * some.fold({ ifSome: console.log, ifNone: () => console.log('none') }); // 5 */ static some(value: Present): Option { return new Some(value); @@ -46,27 +48,31 @@ abstract class Option implements Monad, Matchable, Futurizab * @returns {None} A `None` instance. * @example * const none = Option.none(); - * none.match(console.log, () => console.log('none')); // none + * none.fold({ ifSome: console.log, ifNone: () => console.log('none') }); // none */ static none(): Option { return new None(); } /** - * Creates an `Option` instance from a `Matchable` instance. + * Creates an `Option` instance from a `Foldable` instance. * @template T The type of the value. - * @param {Matchable} matchable The matchable instance. - * @returns {Option} A `Some` instance if the matchable contains a value, otherwise a `None` instance. + * @param {Foldable} foldable The foldable instance. + * @returns {Option} A `Some` instance if the foldable contains a value, otherwise a `None` instance. * @example * const either = Either.right(5); * const option = Option.from(either); - * option.match(console.log, () => console.log('none')); // 5 + * option.fold({ ifSome: console.log, ifNone: () => console.log('none') }); // 5 */ - static from(matchable: Matchable): Option { - return matchable.match>( - (value: T) => Option.of(value), - () => Option.of(undefined) - ); + static from(foldable: Foldable): Option { + return foldable.fold>({ + ifSome: (value: T) => Option.of(value), + ifNone: () => Option.none(), + ifSuccess: (value: T) => Option.of(value), + ifFailure: () => Option.none(), + ifRight: (value: T) => Option.of(value), + ifLeft: () => Option.none(), + }); } /** @@ -75,10 +81,10 @@ abstract class Option implements Monad, Matchable, Futurizab * @returns {T} The value if present, otherwise the default value. * @example * const some = Option.of(5); - * console.log(some.getOrElse(0)); // 5 + * some.getOrElse(0); // 5 * * const none = Option.of(null); - * console.log(none.getOrElse(0)); // 0 + * none.getOrElse(0); // 0 */ abstract getOrElse(otherValue: T): T; @@ -88,56 +94,74 @@ abstract class Option implements Monad, Matchable, Futurizab * @returns {Option} A `Some` instance if the value matches the predicate, otherwise a `None` instance. * @example * const some = Option.of(5).filter(value => value > 3); - * some.match(console.log, () => console.log('none')); // 5 + * some.fold({ ifSome: console.log, ifNone: () => console.log('none') }); // 5 * * const none = Option.of(2).filter(value => value > 3); - * none.match(console.log, () => console.log('none')); // none + * none.fold({ ifSome: console.log, ifNone: () => console.log('none') }); // none */ abstract filter(predicate: (value: T) => boolean): Option; /** * Transforms the value contained in this `Option` instance. + * @template T The type of the value. * @template U The type of the transformed value. * @param {(value: T) => U} transform The transformation function. * @returns {Option} A new `Option` instance containing the transformed value. * @example * const some = Option.of(5).map(value => value * 2); - * some.match(console.log, () => console.log('none')); // 10 + * some.fold({ ifSome: console.log, ifNone: () => console.log('none') }); // 10 */ abstract map(transform: (value: T) => U): Option; /** * Transforms the value contained in this `Option` instance into another `Option` instance. + * @template T The type of the value. * @template U The type of the transformed value. * @param {(value: T) => Option} transform The transformation function. * @returns {Option} The result of the transformation function. * @example * const some = Option.of(5).flatMap(value => Option.of(value * 2)); - * some.match(console.log, () => console.log('none')); // 10 + * some.fold({ ifSome: console.log, ifNone: () => console.log('none') }); // 10 */ abstract flatMap(transform: (value: T) => Option): Option; + /** + * Executes an action if this is a `Some` instance. + * @template T The type of the value. + * @param {(value: T) => void} action The action to execute if this is a `Some` instance. + * @returns {Option} The current `Option` instance. + * @example + * const result = Option.some(5).onSome(value => console.log(value)); // 5 + */ + abstract onSome(action: (value: T) => void): Option; + + /** + * Executes an action if this is a `None` instance. + * @param {() => void} action The action to execute if this is a `None` instance. + * @returns {Option} The current `Option` instance. + * @example + * const result = Option.none().onNone(() => console.log('none')); // none + */ + abstract onNone(action: () => void): Option; + /** * Unwraps the value contained in this `Option` instance by applying the appropriate handler for both Some and None cases. + * @template T The type of the value. * @template U The type of the result. - * @param {(value: T) => U} ifSome The function to call if this is a `Some` instance. - * @param {(_: undefined) => U} ifNone The function to call if this is a `None` instance. - * @returns {U} The result of the matching function. + * @param {FoldingOption} folding The folding object containing the functions to call for each case. + * @returns {U} The result of the folding function. * @example - * const some = Option.of(5); - * some.match(console.log, () => console.log('none')); // 5 - * - * const none = Option.of(null); - * none.match(console.log, () => console.log('none')); // none + * const result = Option.some(5); + * result.fold({ ifSome: console.log, ifNone: () => console.error('Value is empty') }); // 5 */ - abstract match(ifSome: (value: T) => U, ifNone: (_: undefined) => U): U; + abstract fold(folding: FoldingOption): U; /** * Checks if this is a `Some` instance. * @returns {boolean} `true` if this is a `Some` instance, otherwise `false`. * @example * const some = Option.of(5); - * some.match(value => console.log(value.isSome()), () => console.log('none')); // true + * some.isSome(); // true */ abstract isSome(): this is Some; @@ -146,7 +170,7 @@ abstract class Option implements Monad, Matchable, Futurizab * @returns {boolean} `true` if this is a `None` instance, otherwise `false`. * @example * const none = Option.of(null); - * none.match(console.log, none => console.log(none.isNone())); // true + * none.isNone(); // true */ abstract isNone(): this is None; @@ -195,8 +219,17 @@ class Some extends Option { return transform(this.value); } - match(some: (value: T) => U, _: (_: never) => never): U { - return some(this.value); + onSome(action: (value: T) => void): Option { + action(this.value); + return this; + } + + onNone(_: () => void): Option { + return this; + } + + fold(folding: FoldingOption): U { + return folding.ifSome(this.value); } isNone(): this is None { @@ -233,8 +266,17 @@ class None extends Option { return new None(); } - match(_: (_: never) => never, none: (noneValue: undefined) => U): U { - return none(undefined); + onSome(_: (value: T) => void): Option { + return this; + } + + onNone(action: () => void): Option { + action(); + return this; + } + + fold(folding: FoldingOption): U { + return folding.ifNone(undefined); } isNone(): this is None { diff --git a/src/try/try.test.ts b/src/try/try.test.ts index 7611be2..d171ed7 100644 --- a/src/try/try.test.ts +++ b/src/try/try.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { Failure, Success, Try } from './try'; import { Either } from '../either'; import { Option } from '../option'; @@ -18,27 +18,27 @@ describe('Try monad', () => { }); it.each([ - { typeMatchable: 'Right', tryType: 'Success', matchable: Either.right(2), expected: new Success(2) }, + { typeFoldable: 'Right', tryType: 'Success', foldable: Either.right(2), expected: new Success(2) }, { - typeMatchable: 'Left', + typeFoldable: 'Left', tryType: 'Failure', - matchable: Either.left(new Error('An error occurred')), - expected: new Failure(new Error('An error occurred')), + foldable: Either.left('irrelevant'), + expected: new Failure(new Error('Left value: irrelevant')), }, { - typeMatchable: 'Some', + typeFoldable: 'Some', tryType: 'Success', - matchable: Option.of(2), + foldable: Option.of(2), expected: new Success(2), }, { - typeMatchable: 'None', + typeFoldable: 'None', tryType: 'Failure', - matchable: Option.of(undefined), - expected: new Failure(new Error('No error provided')), + foldable: Option.of(undefined), + expected: new Failure(new Error('Empty value')), }, - ])('$tryType should be created from $typeMatchable', ({ matchable, expected }) => { - expect(Try.from(matchable)).toEqual(expected); + ])('$tryType should be created from $typeFoldable', ({ foldable, expected }) => { + expect(Try.from(foldable)).toEqual(expected); }); it.each([ @@ -66,4 +66,40 @@ describe('Try monad', () => { ])('$type should handle isSuccess operation correctly', ({ tryMonad, expected }) => { expect(tryMonad.isSuccess()).toEqual(expected); }); + + it('should retrieve the value from Success', () => { + const success = Try.success(2); + expect(success.getOrThrow()).toEqual(2); + expect(success.getOrElse(3)).toEqual(2); + }); + + it('should throw an error when retrieving the value from Failure', () => { + const failure = Try.failure(new Error('An error occurred')); + expect(() => failure.getOrThrow()).toThrowError(new Error('An error occurred')); + }); + + it('should retrieve a default value from Failure', () => { + const failure = Try.failure(new Error('An error occurred')); + expect(failure.getOrElse(3)).toEqual(3); + }); + + it('should execute an action if is a Success', () => { + const action = vi.fn(); + Try.success(2).onSuccess(action); + expect(action).toHaveBeenCalledWith(2); + + const nonCallableAction = vi.fn(); + Try.failure(new Error('error')).onSuccess(nonCallableAction); + expect(nonCallableAction).not.toHaveBeenCalled(); + }); + + it('should execute an action if is a Failure', () => { + const action = vi.fn(); + Try.failure(new Error('error')).onFailure(action); + expect(action).toHaveBeenCalledWith(new Error('error')); + + const nonCallableAction = vi.fn(); + Try.success(2).onFailure(nonCallableAction); + expect(nonCallableAction).not.toHaveBeenCalled(); + }); }); diff --git a/src/try/try.ts b/src/try/try.ts index 4ce6dd0..89d995a 100644 --- a/src/try/try.ts +++ b/src/try/try.ts @@ -1,13 +1,15 @@ import { Monad } from '../monad'; -import { Matchable } from '../match'; import { Futurizable } from '../futurizable'; import { Future } from '../future'; +import { Foldable, Folding } from '../fold'; + +type FoldingTry = Folding<'Try', T, Error, U>; /** * Abstract class representing a computation that may either result in a value or an error. * @template T The type of the value. */ -abstract class Try implements Monad, Matchable, Futurizable { +abstract class Try implements Monad, Futurizable, Foldable { /** * Executes a function and returns a `Try` instance. * @template T The type of the value. @@ -15,7 +17,7 @@ abstract class Try implements Monad, Matchable, Futurizable { * @returns {Try} A `Success` instance if the function executes without error, otherwise a `Failure` instance. * @example * const result = Try.execute(() => JSON.parse('{"key": "value"}')); - * result.match(console.log, error => console.error(error.message)); // { key: 'value' } + * result.fold({ ifSuccess: console.log, ifFailure: error => console.error(error.message) }); // { key: 'value' } */ static execute(executable: () => T): Try { try { @@ -26,66 +28,118 @@ abstract class Try implements Monad, Matchable, Futurizable { } /** - * Creates a `Try` instance from a `Matchable` instance. + * Creates a `Success` instance. + * @template T The type of the value. + * @param {T} value The value of the successful computation. + * @returns {Success} A `Success` instance containing the value. + * @example + * const success = Try.success(5); + * success.fold({ ifSuccess: console.log, ifFailure: error => console.error(error.message) }); // 5 + */ + static success(value: T): Try { + return new Success(value); + } + + /** + * Creates a `Failure` instance. + * @template T The type of the value. + * @param {Error} error The error of the failed computation. + * @returns {Failure} A `Failure` instance containing the error. + * @example + * const failure = Try.failure(new Error('An error occurred')); + * failure.fold({ ifSuccess: console.log, ifFailure: error => console.error(error.message) }); // An error occurred + */ + static failure(error: Error): Try { + return new Failure(error); + } + + /** + * Creates a `Try` instance from a `Foldable` instance. * @template T The type of the value. - * @param {Matchable} matchable The matchable instance. - * @returns {Try} A `Success` instance if the matchable contains a value, otherwise a `Failure` instance. + * @param {Foldable} foldable The foldable instance. + * @returns {Try} A `Success` instance if the foldable contains a value, otherwise a `Failure` instance. * @example * const some = Option.of(5); * const success = Try.from(some); - * sucess.match(console.log, error => console.error(error.message)); // 5 + * success.fold({ ifSuccess: console.log, ifFailure: error => console.error(error.message) }); // 5 * * const none = Option.of(undefined); * const failure = Try.from(none); - * failure.match(console.log, error => console.error(error.message)); // "No error provided" + * failure.fold({ ifSuccess: console.log, ifFailure: error => console.error(error.message) }); // Empty value */ - static from(matchable: Matchable): Try { - return matchable.match>( - (value: T) => new Success(value), - (error: unknown) => (error instanceof Error ? new Failure(error) : new Failure(new Error('No error provided'))) - ); + static from(foldable: Foldable): Try { + return foldable.fold>({ + ifSuccess: (value: T) => Try.success(value), + ifFailure: (error: unknown) => Try.failure(error as Error), + ifSome: (value: T) => Try.success(value), + ifNone: () => Try.failure(new Error('Empty value')), + ifRight: (value: T) => Try.success(value), + ifLeft: (value: unknown) => Try.failure(new Error('Left value: ' + value)), + }); } /** * Transforms the value contained in this `Try` instance. + * @template T The type of the value. * @template U The type of the transformed value. * @param {(value: T) => U} transform The transformation function. * @returns {Try} A new `Try` instance containing the transformed value. * @example * const result = Try.execute(() => 5).map(value => value * 2); - * result.match(console.log, error => console.error(error.message)); // 10 + * result.fold({ ifSuccess: console.log, ifFailure: error => console.error(error.message) }); // 10 */ abstract map(transform: (value: T) => U): Try; /** * Transforms the value contained in this `Try` instance into another `Try` instance. + * @template T The type of the value. * @template U The type of the transformed value. * @param {(value: T) => Try} transform The transformation function. * @returns {Try} The result of the transformation function. * @example * const result = Try.execute(() => 5).flatMap(value => Try.execute(() => value * 2)); - * result.match(console.log, error => console.error(error.message)); // 10 + * result.fold({ ifSuccess: console.log, ifFailure: error => console.error(error.message) }); // 10 */ abstract flatMap(transform: (value: T) => Try): Try; + /** + * Executes an action if this is a `Success` instance. + * @template T The type of the value. + * @param {(r: T) => void} action The action to execute if this is a `Success` instance. + * @returns {Try} The current `Try` instance. + * @example + * const result = Try.execute(() => 5).onSuccess(value => console.log(value)); // 5 + */ + abstract onSuccess(action: (value: T) => void): Try; + + /** + * Executes an action if this is a `Failure` instance. + * @template T The type of the value. + * @param {(error: Error) => void} action The action to execute if this is a `Failure` instance. + * @returns {Try} The current `Try` instance. + * @example + * const result = Try.execute(() => { throw new Error('failure'); }).onFailure(error => console.error(error.message)); // failure + */ + abstract onFailure(action: (error: Error) => void): Try; + /** * Unwraps the value contained in this `Try` instance by applying the appropriate handler for both Success and Failure cases. + * @template T The type of the value. * @template U The type of the result. - * @param {(value: T) => U} ifSuccess The function to call if this is a `Success` instance. - * @param {(error: Error) => U} ifFailure The function to call if this is a `Failure` instance. - * @returns {U} The result of the matching function. + * @param {FoldingTry} folding The folding object containing the functions to call for each case. + * @returns {U} The result of the folding function. * @example - * const result = Try.execute(() => JSON.parse('invalid json')); - * result.match(console.log, error => console.error(error.message)); // Unexpected token i in JSON at position 0 + * const result = Try.success(5); + * result.fold({ ifSuccess: console.log, ifFailure: (error) => console.error(error.message) }); // 5 */ - abstract match(ifSuccess: (value: T) => U, ifFailure: (error: Error) => U): U; + abstract fold(folding: FoldingTry): U; /** * Checks if this is a `Success` instance. * @returns {boolean} `true` if this is a `Success` instance, otherwise `false`. * @example * const result = Try.execute(() => 5); - * result.match(value => console.log(value.isSuccess()), error => console.error(error.message)); // true + * result.isSuccess(); // true */ abstract isSuccess(): this is Success; @@ -94,10 +148,36 @@ abstract class Try implements Monad, Matchable, Futurizable { * @returns {boolean} `true` if this is a `Failure` instance, otherwise `false`. * @example * const result = Try.execute(() => { throw new Error('failure'); }); - * result.match(console.log, error => console.error(error.isFailure()); // true + * result.isFailure(); // true */ abstract isFailure(): this is Failure; + /** + * Retrieves the value contained in this `Try` instance or a default value if this is a `Failure` instance. + * @param {T} value The default value to return if this is a `Failure` instance. + * @returns {T} The value contained in this `Try` instance or the default value. + * @example + * const result = Try.execute(() => 5); + * console.log(result.getOrElse(0)); // 5 + * + * const failure = Try.execute(() => { throw new Error('failure'); }); + * failure.getOrElse(0); // 0 + */ + abstract getOrElse(value: T): T; + + /** + * Retrieves the value contained in this `Try` instance. + * @returns {T} The value contained in this `Try` instance. + * @throws {Error} If this is a `Failure` instance. + * @example + * const result = Try.execute(() => 5); + * console.log(result.getOrThrow()); // 5 + * + * const failure = Try.execute(() => { throw new Error('failure'); }); + * failure.getOrThrow(); // Error: failure + */ + abstract getOrThrow(): T; + /** * Converts this `Try` instance into a `Future` instance. * @returns {Future} A new `Future` instance. @@ -134,8 +214,17 @@ class Success extends Try { return transform(this.value); } - match(ifSuccess: (value: T) => U, _: (_: never) => U): U { - return ifSuccess(this.value); + onSuccess(action: (value: T) => void): Try { + action(this.value); + return this; + } + + onFailure(_: (error: Error) => void): Try { + return this; + } + + fold(folding: FoldingTry): U { + return folding.ifSuccess(this.value); } isSuccess(): this is Success { @@ -146,6 +235,14 @@ class Success extends Try { return false; } + getOrElse(_: T): T { + return this.value; + } + + getOrThrow(): T { + return this.value; + } + toFuture(): Future { return Future.of(() => Promise.resolve(this.value)); } @@ -177,8 +274,17 @@ class Failure extends Try { return new Failure(this.error); } - match(_: (_: never) => never, ifFailure: (error: Error) => U): U { - return ifFailure(this.error); + onSuccess(_: (value: T) => void): Try { + return this; + } + + onFailure(action: (error: Error) => void): Try { + action(this.error); + return this; + } + + fold(folding: FoldingTry): U { + return folding.ifFailure(this.error); } isSuccess(): this is Success { @@ -189,6 +295,14 @@ class Failure extends Try { return true; } + getOrElse(value: T): T { + return value; + } + + getOrThrow(): T { + throw this.error; + } + toFuture(): Future { return Future.of(() => Promise.reject(this.error)); } diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 9328f15..8905efb 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -2,7 +2,6 @@ "extends": ["./tsconfig.json"], "compilerOptions": { "module": "CommonJS", - "declarationDir": "./dist/cjs", "outDir": "./dist/cjs", } } diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 721c05d..631c11b 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -2,7 +2,6 @@ "extends": ["./tsconfig.json"], "compilerOptions": { "module": "ESNext", - "declarationDir": "./dist/cjs", "outDir": "./dist/esm", } } diff --git a/tsconfig.json b/tsconfig.json index f042756..ebe6410 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "declaration": true, + "declarationDir": "./dist/types", "target": "ESNext", "lib": ["ES2023"], "moduleResolution": "node",