@7urtle/lambda
Version:
Functional programming library in JavaScript.
311 lines (302 loc) • 12.6 kB
JavaScript
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);