UNPKG

@7urtle/lambda

Version:

Functional programming library in JavaScript.

311 lines (302 loc) 12.6 kB
import { deepInspect } from './utils.js'; import { nary } from './arity.js'; import { reduce } from './list.js'; import { Maybe, Nothing } from './Maybe.js'; import { SyncEffect } from './SyncEffect.js'; import { AsyncEffect } from './AsyncEffect.js'; export const Failure = value => ({ value: value, inspect: () => `Failure(${deepInspect(value)})`, isFailure: () => true, isSuccess: () => false, map: () => Failure(value), flatMap: () => Failure(value), catchMap: fn => Failure(fn(value)), bimap: nary(leftFn => _ => Failure(leftFn(value))), ap: () => Failure(value), orOf: a => Success(a), orElse: fn => fn(), orTry: fn => Either.try(fn) }); export const Success = value => ({ value: value, inspect: () => `Success(${deepInspect(value)})`, isFailure: () => false, isSuccess: () => true, map: fn => Success(fn(value)), catchMap: () => Success(value), bimap: nary(_ => rightFn => Success(rightFn(value))), flatMap: fn => fn(value), ap: m => m.map(value), orOf: () => Success(value), orElse: () => Success(value), orTry: () => Success(value) }); /** * Either is an excellent monad for handling error states and it is fairly similar to our monad Maybe. Either.Failure * represents an error state and Either.Success represents a success state. * * Either.of expects a value as its input. Either.of is the same as Either.Success. You can initiate Either * in its error state by Either.Failure. * * You can also initiate it using Either.try which expects a function as an input. It is Failure if an error * or exception is thrown. It is Success if there are no errors or exceptions. * * Either is called Either because it allows you to branch based on an error state. You want to use Either * for situations when you don't know whether there might be an error. It makes the very visible that an error * can occur and it forces the consumer to handle the situation. * * @example * import {either, Either, Success, Failure, upperCaseOf, liftA2} from '@7urtle/lambda'; * * // in the example we randomly give Either a value or throw an error. Either.try() outputs an instance of Either. * const myEither = Either.try(() => Math.random() > 0.5 ? 'random success' : throw 'random failure'); * * // you can also return Either.Failure or Either.Success based on a function logic * const myFunction = Math.random() > 0.5 ? Either.Success('random success') : Either.Failure('random failure'); * * // Success and Failure can be called directly * Success('7urtle') === Either.Success('7urtle'); // => true * Success('7urtle') === Either.of('7urtle'); // => true * Failure('error') === Either.Failure('error'); // => true * * // you could access the actual value like this * myEither.value; // => 'random success' or 'random failure' * * // you can also inspect it by * myEither.inspect(); // => "Success('random success')" or Failure('random failure') * * // Either.of and Either.Success both represent success states * Either.of('some value').inspect() === Success('some value').inspect(); // => true * * // you can check if the value is Failure * myEither.isFailure(); // => true or false * Either.of('abc').isFailure(); // => false * Success('anything').isFailure(); // => false * Failure('anything').isFailure(); // => true * Either.try(() => {throw 'error'}).isFailure(); // => true * * // you can check if the value is Success * myEither.isSuccess(); // => true or false * Either.of('abc').isSuccess(); // => true * Success('anything').isSuccess(); // => true * Failure('anything').isSuccess(); // => false * Either.try(() => {throw 'error'}).isSuccess(); // => false * * // as a functor the value inside is safely mappable (map doesn't execute over Failure) * myEither.map(value => upperCaseOf(value)); * myEither.inspect(); // => "Success('RANDOM SUCCESS')" or "Failure('random failure')" * * // as a monad Either can be safely flat mapped with other Eithers (flatMap doesn't execute over Failure) * Either.of(3).flatMap(a => Either.of(a + 2)).inspect(); // => 'Success(5)' * Failure(3).flatMap(a => Either.of(null)).inspect(); // => 'Failure(3)' * Either.of(3).flatMap(a => a + 2); // => 5 * * // you can use catchMap if you want to map over Failure * Failure('error').catchMap(a => a + 's'); // => Failure('errors') * Success('7urtle').catchMap(a => a + 's'); // => Success('7urtle') * * // you can use bimap to map over both Success and Failure with different functions * Failure('error').bimap(a + ' is left')(a => a + ' is right'); // => Failure('error is left') * Succcess('7urtle').bimap(a + ' is left')(a => a + ' is right'); // => Success('7urtle is right') * * // orOf(a) replaces Failure with Success * Failure('error').orOf('7urtles'); // => Success('7urtle') * Success('7urtle').orOf('tortoise'); // => Success('7urtle') * * // orElse(a -> Either) replaces Failure with the output of orElse function * Failure('error').orElse(() => Success('7urtle')); // => Success('7urtle') * Success('7urtle').orElse(() => Success('tortoise')); // => Success('7urtle') * * // orTry(a -> b) replaces original Fairlure with Either.try * Failure('error').orTry(() => { throw 'i am an error'; }); // => Failure('i am an error') * Failure('error').orTry(() => '7urtle'); // => Success('7urtle') * Success('7urtle').orTry(() => { throw 'i am an error'; }); // => Success('7urtle') * * // as an applicative functor you can apply Eithers to each other especially using liftA2 or liftA3 * const add = a => b => a + b; * liftA2(add)(Either.of(2))(Either.of(3)); // => Success(5) * Either.of(1).map(add).ap(Either.of(2)).inspect(); // => 'Success(3)' * Failure(1).map(add).ap(Either.of(2)).inspect(); // => 'Failure(1)' * Either.of(add).ap(Either.of(1)).ap(Either.of(2)).inspect(); // => 'Success(3)' */ export const Either = { of: value => Success(value), Success: value => Success(value), Failure: value => Failure(value), try: fn => { try { return Success(fn()); } catch(error) { return Failure(error.message || error); } } }; /** * either outputs result of a function onRight if input Either is Success or outputs result of a function onLeft if input Either is Failure. * * either can be called both as a curried unary function or as a standard ternary function. * * @HindleyMilner either :: (a -> b) -> (b -> c) -> Either * * @pure * @param {function} onFailure * @param {function} onSuccess * @param {Either} functorEither * @return {*} * * @example * import {either, Either} from '@7urtle/lambda'; * * either(a => 'error ' + a)(a => 'success ' + a)(Either.of('abc')); // => 'success abc' * either(a => 'error ' + a)(a => 'success ' + a)(Either.Failure('failure')); // => 'error failure' * either(a => 'error ' + a)(a => 'success ' + a)(Either.try(() => throw 'failure')); // => 'error failure' * * // either can be called both as a curried unary function or as a standard ternary function * either(a => 'error ' + a)(a => 'success ' + a)(Either.of('abc')) === either(a => 'error ' + a, a => 'success ' + a, Either.of('abc')); */ export const either = nary(onFailure => onSuccess => functorEither => functorEither.isFailure() ? onFailure(functorEither.value) : onSuccess(functorEither.value) ); /** * mergeEithers outputs Either of array with all Either values depending whether they are Success or Failure. * * @HindleyMilner mergeEithers :: ([Either]) -> Either * * @pure * @param {Either} eithers * @return {Either} * * @example * import { mergeEithers, Either } from '@7urtle/lambda'; * * mergeEithers(Either.of('abc'), Either.of('def')); // => Success(['abc', 'def']) * mergeEithers(Either.of('abc'), Either.Failure('def')); // => Failure(['def']) * mergeEithers(Either.Failure('abc'), Either.of('def')); // => Failure(['abc']) * mergeEithers(Either.Failure('abc'), Either.Failure('def')); // => Failure(['abc', 'def']) */ export const mergeEithers = (...eithers) => reduce (Success([])) ((accumulator, current) => current.isFailure() ? accumulator.isFailure() // current Either is Failure ? Failure([...accumulator.value, current.value]) // accumulator is Failure and current is Failure => update accumulator : Failure([current.value]) // accumulator is Success and current is Failure => return first Failure : accumulator.isFailure() // current Either is Success ? accumulator // accumulator is Failure and current is Success => return accumulator : Success([...accumulator.value, current.value]) // accumulator is Success and current is Success => update accumulator ) (eithers); /** * validateEithers outputs Either of input value if all input functions returns Success or Failure * with array of error messages. * * @HindleyMilner validateEithers :: ([a -> b]) -> a -> Either * * @pure * @param {functions} fns * @param {*} input * @return {Either} * * @example * import { validateEithers, Either, isAtLeast, lengthOf } from '@7urtle/lambda'; * * // mergeEithers is great to be used for validations * const isPasswordLongEnough = password => * isAtLeast(6)(lengthOf(password)) * ? Either.Success(password) * : Either.Failure('Password must have more than 6 characters.'); * * const isPasswordStrongEnough = password => * /[\W]/.test(password) * ? Either.Success(password) * : Either.Failure('Password must contain special characters.'); * * const validatePassword = validateEithers(isPasswordLongEnough, isPasswordStrongEnough); * * validatePassword('LongPa$$word'); // => Success('LongPa$$word') * validatePassword('Pa$$'); // => Failure(['Password must have more than 6 characters.']) * validatePassword('LongPassword'); // => Failure(['Password must contain special characters.']) * validatePassword('Pass'); // => Failure(['Password must have more than 6 characters.', 'Password must contain special characters.']) */ export const validateEithers = (...fns) => input => reduce (Success(input)) ((accumulator, currentFn) => (currentResult => currentResult.isFailure() ? accumulator.isFailure() // currentResult Either is Failure ? Failure([...accumulator.value, currentResult.value]) // accumulator is Failure and currentResult is Failure => update accumulator : Failure([currentResult.value]) // accumulator is Success and currentResult is Failure => return first Failure : accumulator // currentResult Either is Success )(currentFn(input)) ) (fns); /** * eitherToMaybe converts any Either monad to a Maybe monad with * Maybe Nothing if Either is Failure. * * @HindleyMilner eitherToMaybe :: Either -> Maybe * * @pure * @param {Either} eitherMonad * @return {Maybe} * * @example * import { eitherToMaybe, Either } from '@7urtle/lambda'; * * eitherToMaybe(Either.Success('7urtle')); // => Just('7urtle') * eitherToMaybe(Either.Success(undefined)); // => Nothing * eitherToMaybe(Either.Failure('I am an error.')); // => Nothing * eitherToMaybe(Either.Failure('I am an error.')).value; // => 'I am an error.' */ export const eitherToMaybe = eitherMonad => either (() => Nothing) (value => Maybe.of(value)) (eitherMonad); /** * eitherToSyncEffect converts any Either monad to a SyncEffect monad with * error thrown on trigger if Either is Failure. * * @HindleyMilner eitherToSyncEffect :: Either -> SyncEffect * * @pure * @param {Either} eitherMonad * @return {SyncEffect} * * @example * import { eitherToSyncEffect, Either } from '@7urtle/lambda'; * * eitherToSyncEffect(Either.Success('7urtle')).trigger(); // => '7urtle' * eitherToSyncEffect(Either.Failure('I am an error.')).trigger(); // throws 'I am an error.' */ export const eitherToSyncEffect = eitherMonad => either (error => SyncEffect.of(() => { throw error; })) (value => SyncEffect.of(() => value)) (eitherMonad); /** * eitherToAsyncEffect converts any Either monad to a AsyncEffect monad with * rejected error if Either is Failure. * * @HindleyMilner eitherToAsyncEffect :: Either -> AsyncEffect * * @pure * @param {Either} eitherMonad * @return {AsyncEffect} * * @example * import { eitherToAsyncEffect, Either } from '@7urtle/lambda'; * * eitherToAsyncEffect(Either.Success('7urtle')); // resolves to '7urtle' * eitherToAsyncEffect(Either.Failure('I am an error.')); // rejects 'I am an error.' */ export const eitherToAsyncEffect = eitherMonad => either (error => AsyncEffect.of(reject => _ => reject(error))) (value => AsyncEffect.of(_ => resolve => resolve(value))) (eitherMonad);