@7urtle/lambda
Version:
Functional programming library in JavaScript.
238 lines (230 loc) • 8.44 kB
JavaScript
import { deepInspect } from './utils.js';
import { nary } from './arity.js';
import { isNothing } from './conditional.js';
import { reduce } from './list.js';
import { Failure, Success } from './Either.js';
import { SyncEffect } from './SyncEffect.js';
import { AsyncEffect } from './AsyncEffect.js';
export const Nothing = {
value: null,
inspect: () => 'Nothing',
isNothing: () => true,
isJust: () => false,
map: () => Nothing,
flatMap: () => Nothing,
ap: () => Nothing
};
export const Just = value => ({
value: value,
inspect: () => `Just(${deepInspect(value)})`,
isNothing: () => false,
isJust: () => true,
map: fn => Maybe.of(fn(value)),
flatMap: fn => fn(value),
ap: f => f.map(value)
});
/**
* Maybe is one of the simplest and well known monads. Maybe is also quite similar to our monad Either.
*
* Maybe expects a value as its input. It is Nothing if the value is null, undefined, or empty. It returns
* Just for all other cases.
*
* Maybe is called Maybe because it maybe holds a value. You want to use Maybe for situations when you don't
* know whether there is going to be an input. For example for your API endpoint, it makes it very obvious
* that you service may not receive a value by mistake and forces the consumer of Maybe to safely deal with it.
*
* In other languages, Maybe monad can also be called Option monad or Nullable monad.
*
* @example
* import {maybe, Maybe, Just, Nothing, upperCaseOf, liftA2, flatMap, compose, startsWith} from '@7urtle/lambda';
*
* // in the example we randomly give Maybe a value or undefined. Maybe.of() outputs an instance of Maybe.
* const myMaybe = Maybe.of(Math.random() > 0.5 ? 'random success' : undefined);
*
* // you can use Just and Nothing directly
* Just('7urtle') === Maybe.of('7urtle'); // => true
* Just('7urte') === Maybe.Just('7urtle'); // => true
* Nothing === Maybe.of(undefined); // => true
* Nothing === Maybe.Nothing; // => true
*
* // you could access the actual value like this
* myMaybe.value; // => 'random success' or undefined
*
* // you can also inspect it by
* myMaybe.inspect(); // => "Just('random success')" or "Nothing"
*
* // you can check if the value is Nothing
* myMaybe.isNothing(); // => true or false
* Maybe.of('abc').isNothing(); // => false
* Maybe.of([]).isNothing(); // => true
* Just('7urtle').isNothing(); // => false
* Nothing.isNothing(); // => true
*
* // you can check if the value is Just
* myMaybe.isJust(); // => true or false
* Maybe.of(123).isJust(); // => true
* Maybe.of(null).isJust(); // => false
* Just('7urtle').isJust(); // => true
* Nothing.isJust(); // => false
*
* // as a functor the value inside is safely mappable (map doesn't execute over Nothing)
* myMaybe.map(value => upperCaseOf(value));
* myMaybe.inspect(); // => "Just('RANDOM SUCCESS')" or "Nothing"
*
* // as a monad Maybe can be safely flat mapped with other Maybes (flatMap doesn't execute over Nothing)
* Maybe.of(3).flatMap(a => Maybe.of(a + 2)).inspect(); // => 'Just(5)'
* Maybe.of(3).flatMap(a => Maybe.of(null)).inspect(); // => 'Nothing'
* Maybe.of(3).flatMap(a => a + 2); // => 5
*
* // as an applicative functor you can apply Maybes to each other especially using liftA2 or liftA3
* const add = a => b => a + b;
* liftA2(add)(Maybe.of(2))(Maybe.of(3)); // => Just(5)
* Maybe.of(1).map(add).ap(Maybe.of(2)).inspect(); // => 'Just(3)'
* Maybe.of(1).map(add).ap(Maybe.of(null)).inspect(); // => 'Nothing'
* Maybe.of(add).ap(Maybe.of(1)).ap(Maybe.of(2)).inspect(); // => 'Just(3)'
*
* // as an example you can use Maybe to help you work with DOM like this
* Maybe.of(document.querySelector('#iexist')).map(a => a.offsetTop); // => Just(1240)
* Maybe.of(document.querySelector('#idontexist')).map(a => a.offsetTop); // => Nothing
* maybe
* (() => 'error: the object doesnt exist')
* (offsetTop => 'offset from top is ' + offsetTop)
* (Maybe.of(document?.querySelector('#iexist')?.offsetTop));
*
* // to read API request you can use Maybe this way
* const getQuery = body =>
* flatMap
* (a => Maybe.of(a.queryText))
* (Maybe.of(body.queryResult));
*
* // you can use Maybe, Just, and Nothing as output of your functions
* const maybeGetEnvironmentVariable = key => Maybe.of(process?.env?[key]);
* const maybeDIDKeyFromEnvironment =
* compose(
* flatMap(did => startsWith('did:key')(did) ? Just(did) : Nothing),
* maybeGetEnvironmentVariable
* );
*/
export const Maybe = {
of: value => isNothing(value) ? Nothing : Just(value),
Just: value => Just(value),
Nothing: Nothing
};
/**
* maybe outputs result of a function onJust if input Maybe is Just or outputs input error if input Maybe is Nothing.
*
* maybe can be called both as a curried unary function or as a standard ternary function.
*
* @HindleyMilner maybe :: (a -> b) -> (c -> d) -> Maybe -> e
*
* @pure
* @param {functioon} onNothing
* @param {function} onJust
* @param {Maybe} functorMaybe
* @return {*}
*
* @example
* import {maybe, Maybe} from '@7urtle/lambda';
*
* maybe(() => 'error')(value => value)(Maybe.of('abc')); // => 'abc'
* maybe(() => 'error')(value => value)(Maybe.of(undefined)); // => 'error'
* maybe(() => 'error')(() => 'not error)(Maybe.of(undefined)) === Maybe.of(undefined).isNothing() ? 'error' ? 'not error';
*
* // maybe can be called both as a curried unary function or as a standard ternary function
* maybe(() => 'error')(value => value)(Maybe.of('abc')) === maybe('error', value => value, Maybe.of('abc'));
*/
export const maybe = nary(onNothing => onJust => functorMaybe =>
functorMaybe.isNothing()
? onNothing()
: onJust(functorMaybe.value));
/**
* mergeMaybes outputs Maybe of array with all Maybe values depending whether they are Nothing or Just.
*
* @HindleyMilner mergeMaybes :: ([Maybe]) -> Maybe
*
* @pure
* @param {Maybe} maybes
* @return {Maybe}
*
* @example
* import { mergeMaybes, Nothing, Just, Maybe } from '@7urtle/lambda';
*
* mergeMaybes(Maybe.of('abc'), Just('def')); // => Just(['abc', 'def'])
* mergeMaybes(Maybe.of('abc'), Nothing); // => Nothing
* mergeMaybes(Nothing, Maybe.of('def')); // => Nothing
* mergeMaybes(Nothing, Nothing); // => Nothing
*/
export const mergeMaybes = (...maybes) =>
reduce
(Just([]))
((accumulator, current) =>
current.isNothing()
? Nothing
: accumulator.isNothing()
? Nothing
: Just([...accumulator.value, current.value])
)
(maybes);
/**
* maybeToEither converts any Maybe monad to an Either monad with
* 'Maybe is Nothing.' Failure if the Maybe is Nothing.
*
* @HindleyMilner maybeToEither :: Maybe -> Either
*
* @pure
* @param {Maybe} maybeMonad
* @return {Either}
*
* @example
* import { maybeToEither, Maybe } from '@7urtle/lambda';
*
* maybeToEither(Maybe.of('7urtle')); // => Success('7urtle')
* maybeToEither(Maybe.of(undefined)); // => Failure('Maybe is Nothing.')
*/
export const maybeToEither = maybeMonad =>
maybe
(() => Failure('Maybe is Nothing.'))
(value => Success(value))
(maybeMonad);
/**
* maybeToSyncEffect converts any Maybe monad to an SyncEffect monad with
* 'Maybe is Nothing.' thrown error if the Maybe is Nothing.
*
* @HindleyMilner maybeToSyncEffect :: Maybe -> SyncEffect
*
* @pure
* @param {Maybe} maybeMonad
* @return {SyncEffect}
*
* @example
* import { maybeToSyncEffect, Maybe } from '@7urtle/lambda';
*
* maybeToSyncEffect(Maybe.of('7urtle')).trigger(); // => '7urtle'
* maybeToSyncEffect(Maybe.of(undefined)).trigger(); // throws 'Maybe is Nothing.'
*/
export const maybeToSyncEffect = maybeMonad =>
maybe
(() => SyncEffect.of(() => { throw 'Maybe is Nothing.' }))
(value => SyncEffect.of(() => value))
(maybeMonad);
/**
* maybeToAsyncEffect converts any Maybe monad to an AsyncEffect monad with
* 'Maybe is Nothing.' reject if the Maybe is Nothing.
*
* @HindleyMilner maybeToAsyncEffect :: Maybe -> AsyncEffect
*
* @pure
* @param {Maybe} maybeMonad
* @return {AsyncEffect}
*
* @example
* import { maybeToAsyncEffect, Maybe } from '@7urtle/lambda';
*
* maybeToAsyncEffect(Maybe.of('7urtle')); // resolves to '7urtle'
* maybeToAsyncEffect(Maybe.of(undefined)); // rejects 'Maybe is Nothing.'
*/
export const maybeToAsyncEffect = maybeMonad =>
maybe
(() => AsyncEffect.of(reject => _ => reject('Maybe is Nothing.')))
(value => AsyncEffect.of(_ => resolve => resolve(value)))
(maybeMonad);