diff --git a/README.md b/README.md index 1c16483..255cdc2 100644 --- a/README.md +++ b/README.md @@ -12,9 +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) - * [Recovering from a Left value](#recovering-from-a-left-value) - * [Running side effects](#running-side-effects) - * [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) @@ -26,18 +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) - * [Running side effects](#running-side-effects-1) - * [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) - * [Running side effects](#running-side-effects-2) - * [Retrieving the value](#retrieving-the-value) - * [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) @@ -110,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') @@ -125,7 +127,7 @@ const right = Either.right(42).map(x => x + 1); // Right(43) const left = Either.left('Error').mapLeft(err => `New ${err}`); // Left('New Error') ``` -### Recovering from a Left value +#### Recovering from a Left value You can use the `recover` method to recover from a `Left` value and transform it into a `Right`. @@ -145,7 +147,7 @@ const leftEven = Either.left(42).recover(recoverIfEven); // Righ const leftOdd = Either.left(43).recover(recoverIfEven); // Left('Not even') ``` -### Running side effects +#### Running side effects You can use the `onRight` method to run side effects on the value inside a `Right`. @@ -165,22 +167,22 @@ const right = Either.right(42).onLeft(err => console.log(err)); // No execution const left = Either.left('Error').onLeft(err => console.log(err)); // 'Error' ``` -#### Matching an Either +#### 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 @@ -213,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 ``` @@ -237,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 @@ -289,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 @@ -302,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 @@ -317,7 +323,7 @@ const some = Option.of(42).map(x => x + 1); // Some(43) const none = Option.of(null).map(x => x + 1); // None ``` -### Running side effects +#### Running side effects You can use the `onSome` method to run side effects on the value inside a `Some`. @@ -337,22 +343,22 @@ const some = Option.some(42).onNone(_ => console.log('Empty value')); // No exec const none = Option.none().onNone(_ => console.log('Empty value')); // 'Empty value' ``` -#### Matching an Option +#### Folding an Option -You can use the `match` method to handle both `Some` and `None` cases and unwrap the result. +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).match( - x => x + 1, - () => 'No value' -); // 43 +const some = Option.of(42).fold({ + ifSome: x => `${x + 1}`, + ifNone: () => 'No value', +}); // '43' -const none = Option.of(null).match( - x => x + 1, - () => 'No value' -); // 'No value' +const none = Option.of(null).fold({ + ifSome: x => `${x + 1}`, + ifNone: () => 'No value', +}); // 'No value' ``` #### Checking if an Option is Some or None @@ -399,17 +405,19 @@ 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'; + +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. @@ -419,7 +427,7 @@ import { Try } from '@leanmind/monads'; const success = Try.success(42).flatMap(x => Try.success(x + 1)); // Success(43) ``` -### Running side effects +#### Running side effects You can use the `onSuccess` method to run side effects on the value inside a `Success`. @@ -439,14 +447,14 @@ const succcess = Try.succcess(42).onFailure(err => console.log(err)); // No exec const failure = Try.failure(new Error('Error')).onFailure(err => console.log(err)); // Error('Error') ``` -### Retrieving the value +#### 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.sucess(42); +const success = Try.success(42); const value = success.getOrElse(0); // 42 const failure = Try.failure(new Error('Error')); @@ -465,25 +473,25 @@ const failure = Try.failure(new Error('Error')); const otherValue = failure.getOrThrow(); // throws Error('Error') ``` -### Matching a Try +#### Folding a Try -You can use the `match` method to handle both `Success` and `Failure` cases and unwrap the result. +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.sucess(42).match( - err => `Error: ${err}`, - x => `${x + 1}` -); // '43' +const success = Try.success(42).fold({ + ifSuccess: x => `${x + 1}`, + ifFailure: err => `Error: ${err}`, +}); // '43' -const failure = Try.failure(new Error('Error')).match( - err => `Error: ${err}`, - x => `${x + 1}` -); // 'Error: Error' +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 +#### Handling errors in Infrastructure code Normally, Try is used to handle `Exceptions` that are raise by third party libraries @@ -493,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. @@ -598,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/src/either/either.test.ts b/src/either/either.test.ts index f3944c0..d230004 100644 --- a/src/either/either.test.ts +++ b/src/either/either.test.ts @@ -17,15 +17,15 @@ 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([ @@ -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 }, diff --git a/src/either/either.ts b/src/either/either.ts index 6f5fa6f..f750f36 100644 --- a/src/either/either.ts +++ b/src/either/either.ts @@ -1,14 +1,16 @@ 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 L The type of the left value. @@ -17,7 +19,7 @@ abstract class Either implements Monad, Matchable, Futurizable * @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: R): Either { return new Right(value); @@ -31,28 +33,33 @@ abstract class Either implements Monad, Matchable, Futurizable * @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: 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); } /** @@ -62,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 { @@ -80,7 +87,7 @@ abstract class Either implements Monad, Matchable, Futurizable * @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; @@ -93,10 +100,11 @@ abstract class Either implements Monad, Matchable, Futurizable * @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. @@ -105,9 +113,8 @@ abstract class Either implements Monad, Matchable, Futurizable * @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. @@ -118,7 +125,7 @@ abstract class Either implements Monad, Matchable, Futurizable * @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; @@ -131,7 +138,7 @@ abstract class Either implements Monad, Matchable, Futurizable * @returns {Either} A new `Either` instance containing the transformed value. * @example * const result = Either.left('error').recover(value => Either.right(`Recovered: ${value}`)); - * result.match(console.log, console.error); // Recovered: error + * result.fold({ ifRight: console.log, ifLeft: error => console.error(error.message) }); // Recovered: error */ abstract recover(transform: (l: L) => Either): Either; @@ -162,21 +169,20 @@ abstract class Either implements Monad, Matchable, Futurizable * @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; @@ -185,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; @@ -247,8 +253,8 @@ class Left extends Either { return this; } - match(_: (_: R) => T, ifLeft: (l: L) => T): T { - return ifLeft(this.value); + fold(folding: FoldingEither): T { + return folding.ifLeft(this.value); } isLeft(): this is Left { @@ -307,8 +313,8 @@ class Right extends Either { return this; } - match(ifRight: (r: R) => T, _: (_: L) => T): T { - return ifRight(this.value); + 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 7961050..f09c093 100644 --- a/src/option/option.test.ts +++ b/src/option/option.test.ts @@ -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([ diff --git a/src/option/option.ts b/src/option/option.ts index c36bdea..3768115 100644 --- a/src/option/option.ts +++ b/src/option/option.ts @@ -1,14 +1,16 @@ 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,10 +94,10 @@ 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; @@ -103,7 +109,7 @@ abstract class Option implements Monad, Matchable, Futurizab * @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; @@ -115,7 +121,7 @@ abstract class Option implements Monad, Matchable, Futurizab * @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; @@ -142,24 +148,20 @@ abstract class Option implements Monad, Matchable, Futurizab * 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; @@ -168,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; @@ -226,8 +228,8 @@ class Some extends Option { return this; } - match(some: (value: T) => U, _: (_: never) => never): U { - return some(this.value); + fold(folding: FoldingOption): U { + return folding.ifSome(this.value); } isNone(): this is None { @@ -273,8 +275,8 @@ class None extends Option { return this; } - match(_: (_: never) => never, none: (noneValue: undefined) => U): U { - return none(undefined); + 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 880429a..d171ed7 100644 --- a/src/try/try.test.ts +++ b/src/try/try.test.ts @@ -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([ diff --git a/src/try/try.ts b/src/try/try.ts index eeeee7d..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 { @@ -32,7 +34,7 @@ abstract class Try implements Monad, Matchable, Futurizable { * @returns {Success} A `Success` instance containing the value. * @example * const success = Try.success(5); - * success.match(console.log, error => console.error(error.message)); // 5 + * success.fold({ ifSuccess: console.log, ifFailure: error => console.error(error.message) }); // 5 */ static success(value: T): Try { return new Success(value); @@ -45,31 +47,35 @@ abstract class Try implements Monad, Matchable, Futurizable { * @returns {Failure} A `Failure` instance containing the error. * @example * const failure = Try.failure(new Error('An error occurred')); - * failure.match(console.log, error => console.error(error.message)); // 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 `Matchable` instance. + * 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)), + }); } /** @@ -80,7 +86,7 @@ abstract class Try implements Monad, Matchable, Futurizable { * @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; @@ -92,7 +98,7 @@ abstract class Try implements Monad, Matchable, Futurizable { * @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; @@ -120,21 +126,20 @@ abstract class Try implements Monad, Matchable, Futurizable { * 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; @@ -143,7 +148,7 @@ 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; @@ -156,7 +161,7 @@ abstract class Try implements Monad, Matchable, Futurizable { * console.log(result.getOrElse(0)); // 5 * * const failure = Try.execute(() => { throw new Error('failure'); }); - * console.log(failure.getOrElse(0)); // 0 + * failure.getOrElse(0); // 0 */ abstract getOrElse(value: T): T; @@ -169,7 +174,7 @@ abstract class Try implements Monad, Matchable, Futurizable { * console.log(result.getOrThrow()); // 5 * * const failure = Try.execute(() => { throw new Error('failure'); }); - * console.log(failure.getOrThrow()); // Error: failure + * failure.getOrThrow(); // Error: failure */ abstract getOrThrow(): T; @@ -218,8 +223,8 @@ class Success extends Try { return this; } - match(ifSuccess: (value: T) => U, _: (_: never) => U): U { - return ifSuccess(this.value); + fold(folding: FoldingTry): U { + return folding.ifSuccess(this.value); } isSuccess(): this is Success { @@ -278,8 +283,8 @@ class Failure extends Try { return this; } - match(_: (_: never) => never, ifFailure: (error: Error) => U): U { - return ifFailure(this.error); + fold(folding: FoldingTry): U { + return folding.ifFailure(this.error); } isSuccess(): this is Success {