@7urtle/lambda
Version:
Functional programming library in JavaScript.
447 lines (432 loc) • 14.2 kB
JavaScript
import { reduce, reduceRight } from './list.js';
import { isString, isArray, isObject } from './conditional.js';
import { minusOneToUndefined, passThrough } from './utils.js';
import { nary } from "./arity.js";
/**
* identity is a function that simply passes its input to its output without changing it.
*
* @HindleyMilner identity :: a -> a
*
* @pure
* @param {*} anything
* @return {*}
*
* @example
* import {identity} from '@7urtle/lambda';
*
* identity('anything');
* // => anything
*/
export const identity = anything => anything;
/**
* compose is a right-to-left function composition
* where each function receives input and hands over its output to the next function.
*
* compose executes functions in reverse order to pipe.
*
* compose(f,g)(x) is equivalent to f(g(x)).
*
* @HindleyMilner compose :: [(a -> b)] -> a -> b
*
* @pure
* @param {function} fns
* @param {*} anything
* @return {*}
*
* @example
* import {compose} from '@7urtle/lambda';
*
* const addA = a => a + 'A';
* const addB = a => a + 'B';
* const addAB = value => compose(addA, addB)(value);
*
* addAB('Order: ');
* // => Order: BA
*/
export const compose = (...fns) => anything => reduceRight(anything)((v, f) => f(v))(fns);
/**
* pipe output is a left-to-right function composition
* where each function receives input and hands over its output to the next function.
*
* pipe executes functions in reverse order to compose.
*
* pipe(f,g)(x) is equivalent to g(f(x)).
*
* @HindleyMilner pipe :: [(a -> b)] -> a -> b
*
* @pure
* @param {function} fns
* @param {*} anything
* @return {*}
*
* @example
* import {pipe} from '@7urtle/lambda';
*
* const addA = a => a + 'A';
* const addB = a => a + 'B';
* const addAB = value => pipe(addA, addB)(value);
*
* addAB('Order: ');
* // => Order: AB
*/
export const pipe = (...fns) => anything => reduce(anything)((v, f) => f(v))(fns);
/**
* map executes mapper function over input. If the input is array or monad, their map functions
* are executed. If the input is anything else the mapper function is executed with the input
* as its argument.
*
* In case of monads, you should use map when you want to work with functors using functions
* and functional composition rather than calling Functor.map.
*
* If you need to both filter and map over an array, consider using the filterMap function.
*
* map can be called both as a curried unary function or as a standard binary function.
*
* @HindleyMilner map :: (a -> b) -> a -> b
*
* @param {function} mapper
* @param {*} anything
* @return {*}
*
* @example
* import {map, Maybe, upperCaseOf} from '@7urtle/lambda';
*
* const mapper = a => a + 'm';
* const list = ['a', 'b', 'c'];
*
* // the function mapper is applied to each member of the array
* map(mapper)(list); // => ['am', 'bm', 'cm']
*
* // the function upperCaseOf is applied to the value of the functor
* map(upperCaseOf)(Maybe.of('something')); // => Just('SOMETHING')
*
* // the function upperCaseOf is applied to the input string
* map(upperCaseOf)('turtle'); // => 'TURTLE'
*
* // use of map equals the use of map on the functor
* map(upperCaseOf)(Maybe.of('something')).value === Maybe.of('something').map(upperCaseOf).value;
*
* // map can be called both as a curried unary function or as a standard binary function
* map(upperCaseOf)(Maybe.of('something')).value === map(upperCaseOf, Maybe.of('something')).value;
*/
export const map = nary(mapper => anything =>
anything && anything.map
? anything.map(mapper)
: mapper(anything)
);
/**
* flatMap maps function over inputted functor outputting resulting flattened functor.
*
* You should use flatMap when you want to work with functors using functions
* and functional composition rather than calling flatMaps.
*
* The function can be called both as a unary flatMap(fn)(functor) and binary flatMap(fn, functor).
*
* @HindleyMilner flatMap :: (a -> Functor) -> Functor -> Functor
*
* @param {function} fn
* @param {functor} functor
* @return {functor}
*
* @example
* import {flatMap, map, Maybe} from '@7urtle/lambda';
*
* const maybePlus2 = number => Maybe.of(number + 2);
*
* // the function maybePlus2 is applied to the value of the functor
* flatMap(maybePlus2)(Maybe.of(3)); // => Just(5)
* map(maybePlus2)(Maybe.of(3)); // => Just(Just(5))
*
* // use of flatMap equals the use of flatMap on the functor
* flatMap(maybePlus2)(Maybe.of(3)).value === Maybe.of(3).flatMap(maybePlus2).value;
*
* // flatMap can be called both as a curried unary function or as a standard binary function
* flatMap(maybePlus2)(Maybe.of(3)).value === flatMap(maybePlus2, Maybe.of(3)).value;
*/
export const flatMap = nary(fn => functor => functor.flatMap(fn));
/**
* liftA2 provides point-free way of writing calls over applicative functors and functions expecting 2 inputs. It
* applies input function over both functors values providing a resulting functor.
*
* The function can be called both as a unary liftA2(fn)(functor)(functor) and ternary liftA2(fn, functor, functor).
*
* @HindleyMilner liftA2 (a -> b -> c) -> Applicative a -> Applicative b -> Applicative c
*
* @pure
* @param {function} fn
* @param {functor} ap1
* @param {functor} ap2
* @return {functor}
*
* @example
* import {liftA2, Maybe} from '@7urtle/lambda';
*
* const add = a => b => a + b;
*
* // function add which expects two inputs is applied to the values of two applicative functors Maybe
* // the result is a Maybe functor with the internal value 5
* liftA2(add)(Maybe.of(2))(Maybe.of(3)); // => Just(5)
*
* // an example of applying a function over a Maybe of undefined value to demonstrate continued safety of functors
* liftA2(add)(Maybe.of(1))(Maybe.of(undefined)).isNothing(); // => true
*
* // liftA2 can be called both as a curried unary function or as a standard ternary function
* liftA2(add)(Maybe.of(2))(Maybe.of(3)).value === liftA2(add, Maybe.of(2), Maybe.of(3)).value;
*/
export const liftA2 = nary(fn => ap1 => ap2 => ap1.map(fn).ap(ap2));
/**
* liftA3 provides point-free way of writing calls over applicative functors and functions expecting 3 inputs. It
* applies input function over input functors values providing a resulting functor.
*
* The function can be called both as a unary liftA3(fn)(functor)(functor)(functor) and quaternary liftA2(fn, functor, functor, functor).
*
* @HindleyMilner liftA3 (a -> b -> c -> d) -> Applicative a -> Applicative b -> Applicative c -> Applicative d
*
* @pure
* @param {function} fn
* @param {functor} ap1
* @param {functor} ap2
* @param {functor} ap3
* @return {functor}
*
* @example
* import {liftA3, Maybe} from '@7urtle/lambda';
*
* const add = a => b => c => a + b + c;
*
* // function add which expects three inputs is applied to the values of three applicative functors Maybe
* // the result is a Maybe functor with the internal value 9
* liftA3(add)(Maybe.of(2))(Maybe.of(3))(Maybe.of(4)); // => Just(9)
*
* // an example of applying a function over a Maybe of undefined value to demonstrate continued safety of functors
* liftA3(add)(Maybe.of(1))(Maybe.of(2))(Maybe.of(undefined)).isNothing(); // => true
*
* // liftA3 can be called both as a curried unary function or as a standard quaternary function
* liftA3(add)(Maybe.of(2))(Maybe.of(3))(Maybe.of(4)).value === liftA3(add, Maybe.of(2), Maybe.of(3), Maybe.of(4)).value;
*/
export const liftA3 = nary(fn => ap1 => ap2 => ap3 => ap1.map(fn).ap(ap2).ap(ap3));
/**
* contact outputs concatenated inputs of strings, arrays and shallow objects or outputs undefined for other types.
*
* concat can be called both as a curried unary function or as a standard binary function.
*
* @HindleyMilner concat :: a -> a|boolean
*
* @pure
* @param {*} a
* @param {*} b
* @return {*}
*
* @example
* import {concat} from '@7urtle/lambda';
*
* concat('cd')('ab'); // => 'abcd'
* concat([3, 4])([1,2]); // => [1, 2, 3, 4]
* concat({here: {here: 'there'}})({hi: 'hello'}); // => {hi: 'hello', here: {here: 'there'}}
* concat('cd')(1); // => undefined
*
* // concat can be called both as a curried unary function or as a standard binary function
* concat('cd')('ab') === concat('cd', 'ab');
*/
export const concat = nary(a => b =>
isString(b) || isArray(b)
? b.concat(a)
: isObject(b)
? { ...b, ...a }
: undefined);
/**
* merge performs a deep merge on all input objects and arrays.
*
* @HindleyMilner merge :: [a] -> [b]
*
* @pure
* @param {array|object} sources
* @return {array|object}
*
* @example
* import {merge} from '@7urtle/lambda';
*
* const obj1 = { a: 'a', c: ['a'] };
* const obj2 = { b: a => a, d: ['a', 'b'] };
* const obj3 = { a: 'c', c: ['c'] };
*
* merge(obj1, obj2, obj3));
* // => {"a": "c", "b": a => a, "c": ["a", "c"], "d": ["a", "b"]}
*
* const list1 = ['a', 'b'];
* const list2 = [1, 2];
*
* merge(list1,list2);
* // => ['a', 'b', 1, 2]
*
* merge(list1, obj1)
* // => {"0": "a", "1": "b", "a": "a", "c": ["a"]}
*/
export const merge = (...sources) =>
reduce
([])
((acc, current) =>
isArray(current)
? [...acc, ...current]
: isObject(current)
? reduce
(acc)
((a, c) =>
isObject(current[c]) && c in acc
? { ...a, [c]: merge(acc[c], current[c]) }
: { ...a, [c]: current[c] }
)
(Object.getOwnPropertyNames(current))
: { ...acc, ...current }
)
(sources);
/**
* includes(a)(b) output is true if b includes a.
*
* includes can be called both as a curried unary function or as a standard binary function.
*
* @HindleyMilner includes :: a -> b -> boolean
*
* @pure
* @param {*} a
* @param {*} b
* @return {*}
*
* @example
* import {includes} from '@7urtle/lambda';
*
* includes('rt')('7urtle'); // => true
* includes(1)([1, 2, 3]) // => true
* includes('turtle')([1, 2, 3]) // => false
*
* // includes can be called both as a curried unary function or as a standard binary function
* includes('rt')('7urtle') === includes('rt', '7urtle');
*/
export const includes = nary(a => b => b.includes(a));
/**
* indexOf(a)(b) outputs position of input a within input b or undefined if it is not found.
*
* indexOf can be called both as a curried unary function or as a standard binary function.
*
* @HindleyMilner indexOf :: a -> b -> number
*
* @pure
* @param {*} a
* @param {*} b
* @return {*}
*
* @example
* import {indexOf} from '@7urtle/lambda';
*
* indexOf('7')('7urtle'); // => 0
* indexOf(7)('7urtle'); // => 0
* indexOf(2)([1, 2, 3]); // => 1
* indexOf(4)([1, 2, 3]); // => undefined
*
* // indexOf can be called both as a curried unary function or as a standard binary function
* indexOf('7')('7urtle') === indexOf('7', '7urtle');
*/
export const indexOf = nary(a => b => minusOneToUndefined(b.indexOf(a)));
/**
* lastIndexOf(a)(b) outputs position of input a withing input b looking from the end or it retuns undefined if it is not found.
*
* lastIndexOf can be called both as a curried unary function or as a standard binary function.
*
* @HindleyMilner lastIndexOf :: a -> b -> number
*
* @pure
* @param {*} a
* @param {*} b
* @return {*}
*
* @example
* import {lastIndexOf} from '@7urtle/lambda';
*
* lastIndexOf('urtle')('7urtle'); // => 1
* lastIndexOf(2)([1, 2, 3, 2]); // => 3
* lastIndexOf('8')('7urtle'); // => undefined
*
* // lastIndexOf can be called both as a curried unary function or as a standard binary function
* lastIndexOf('7')('7urtle') === lastIndexOf('7', '7urtle');
*/
export const lastIndexOf = nary(a => b => minusOneToUndefined(b.lastIndexOf(a)));
/**
* memoize uses input memory to save output of input function and then uses it to lookup the result on a repeated run. This
* function is not pure because the input memory is modified in the process.
*
* The function can be called both as a curried unary function or as a standard ternary function.
*
* @HindleyMilner memoize :: object -> (a -> b) -> a -> b
*
* @param {object} memory
* @param {function} fn
* @param {*} anything
* @return {*}
*
* @example
* import {memoize} from '@7urtle/lambda';
*
* const addTwo = a => a + 2;
* let memory = {};
*
* memoize(memory)(addTwo)(1); // => 3
* memoize(memory)(addTwo)(1); // => 3
* memory[1]; // => 3
*
* // lastIndexOf can be called both as a curried unary function or as a standard ternary function
* memoize(memory)(addTwo)(1) === memoize(memory, addTwo, 1);
*/
export const memoize = nary(memory => fn => anything =>
anything in memory
? memory[anything]
: passThrough(b => memory[anything] = b)(fn(anything))
);
/**
* memo takes input function and returns it enhanced by memoization which ensures that each result is
* always remembered internally and executed only once.
*
* @HindleyMilner memo :: (a -> b) -> (a -> b)
*
* @pure
* @param {function} fn
* @return {function}
*
* @example
* import {memo} from '@7urtle/lambda';
*
* const addTwo = a => a + 2;
* const memoAddTwo = memo(addTwo);
* const memoAddThree = memo(a => a + 3);
*
* memoAddTwo(1); // => 3
* memoAddThree(1); // => 4
*
* let count = 0;
* const increaseCount = () => ++count;
*
* increaseCount(); // 1
* increaseCount(); // 2
*
* const memoIncreaseCount = memo(increaseCount);
*
* memoIncreaseCount(); // 3
* memoIncreaseCount(); // 3
* memoIncreaseCount(); // 3
*/
export const memo = fn => memoize({})(fn);
/**
* fail throws the input error. It is just a function wrapped around JavaScript throw.
*
* @HindleyMilner fail :: a -> b -> number
*
* @impure
* @param {string|Error} error
* @return {null}
*
* @example
* import { fail } from '@7urtle/lambda';
*
* fail('I am an error.'); // => throws 'I am an error.'
* fail(new Error('something happend :(')); // => throws Error('something happened :('))
*/
export const fail = error => { throw error; };