kompis
A collection of plain and higher order functions for doing composition stuff in javascript.
Usage
npm install kompis
All exports are named. Example:
import { Pipe, map, add } from "kompis";
Pipe(map(add(10)));
import * as K from "kompis";
K.Pipe(K.map(K.add(10)));
ES6
ES6 source can be imported from kompis/es6
(it's about a third of the size of the regular ES5 version).
About
The functions can be divided in two groups: core functions and utils. The utils are properly documented below. The core functions are all pretty small so their implementations are included (without further documentation) at the bottom of this readme.
Argument order
Higher order functions that accept more than one thing accept the data to operate on last (opposite of native js counerparts). This makes partial application more practical.
"A".padStart(3, "_");
[1, 2, 3].map(n => n + 1);
padStart(3, "_")("A");
map(add(1))([1, 2, 3]);
Note: this also applies to math functions like subtract
and divide
:
2 / 4;
const divideByTwo = divide(2);
divideByTwo(4);
divide(2)(4);
Utils
array
array(length: string, mapper: function, filter: function): any[]
Inspired by list comprehension in other languages.
In python you would do this:
[i * i for i in range(5) if i % 2 == 0]
With array
this becomes:
array(5, i => i * i, i => i % 2 === 0);
array(5, pow(2), isEven);
In both cases, the output is [0, 4, 16]
get
get(path: string, default: any): (obj: object | array) => any
Safely get children properties of an object (like lodash/get
but with the object last).
const obj = { a: ["nope", "yep"] };
get("a")(obj);
get("a[1]")(obj);
get("a.b.c", {})(obj);
match
match(...patterns: [predicate: function, map: function][]): (value: any) => any
Inspired by (but not the same as) pattern matching from other functional languages.
const matcher = match([isEven, v => `${v} is even!`], [isOdd, v => `${v} is odd!`]);
matcher(1);
matcher(2);
matcher("a");
Recursion
If you use match
recursively you'll get a maximum call stack exceeded error. To avoid this, execute match
with a value explicitly if you need recursion:
const badMatch = match([somePredicate, badMatch], [otherwise, n => n]);
const goodMatch = value => match([somePredicate, goodMatch], [otherwise, n => n])(value);
Fallback value
If you need a fallback pattern, you can use the otherwise
function in the last pattern (otherwise
always returns true
).
const matcher = match(
[isEven, v => `${v} is even!`],
[isOdd, v => `${v} is odd!`],
[otherwise, () => "No match"]
);
matcher(1);
matcher(2);
matcher("a");
not
not(predicate: function): (...args: any[]) => boolean
For a partially applied predicate (or a non-higher-order predicate), returns the inverse of the predicate result. Handy for avoiding arrow functions when negating predicates.
const isNotString = not(isString);
isString("hello");
isNotString("hello");
Pipe
Pipe(...funcs: function[]): function
Performs left-to-right function composition. Mostly like Ramdas pipe except all functions must be unary.
const addTwoAndDouble = makePipe(add(2), multiply(2));
addTwoAndDouble(1);
[1, 2].map(addTwoAndDouble);
trace
trace(label: string): (value: any_) => any
Accepts a label and then a single value, logs it using console.log
and returns the value. Useful for debugging pipelines.
Pipe(
add(2),
trace(),
multiply(2),
trace("final:")
)(1);
Notable core functions
reduce
reduce(reducer: function, initial: any, map?: function, filter?: function): (arr: any[]) => any[]
Note that reducer
needs to be a higher order unary function (returning another unary function) and that the order of the current and accumulator are reversed. This makes it possible to use other functions from this package as the reducer
.
reduce(curr => accum => accum + curr, 0)([1, 2, 3]);
reduce(add, 0)([1, 2, 3]);
reduce(concat, [])([[1], [2], [3]]);
map
and filter
can be used to do many operations that otherwise would require iterating over a list many times, like [].map(fn).filter(fn).reduce(fn)
which can be orders of magnitude slower.
const numbers = [1, 2, 3, 4];
reduce(add, 0, pow(2), isEven)(numbers);
Pipe(filter(isEven), map(pow(2)), reduce(add))(numbers);
Core functions
import { get, match, not, otherwise, Pipe, trace } from "./utils";
export { get, match, not, otherwise, Pipe, trace };
export const exists = a => a !== undefined && a !== null;
export const id = x => x;
export const or = fallback => v => (exists(v) ? v : fallback);
export const no = () => false;
export const noop = () => {};
export const yes = () => true;
export const mapIf = (predicate, ifFunc = id, elseFunc = id) => v =>
predicate(v) ? ifFunc(v) : elseFunc(v);
export const gt = b => a => a > b;
export const gte = b => a => a >= b;
export const lt = b => a => a < b;
export const lte = b => a => a <= b;
export const is = a => b => a === b;
export const isNumber = n => typeof n === "number";
export const isString = n => typeof n === "string";
export const isEven = n => isNumber(n) && n % 2 === 0;
export const isOdd = n => isNumber(n) && n % 2 !== 0;
export const isAtPath = (path, predicate) => v => predicate(get(path)(v));
export const isAtIndex = (index, predicate) => isAtPath(`[${index}]`, predicate);
export const isAll = (...predicates) => v =>
predicates.reduce((a, pred) => a && pred(v), predicates.length ? true : false);
export const isSome = (...predicates) => v =>
predicates.reduce((a, pred) => a || pred(v), false);
const sS = str => (exists(str) && typeof str === "string" ? str : "");
export const charCodeAt = index => str => sS(str).charCodeAt(index);
export const endsWith = term => str => sS(str).endsWith(term);
export const fromCharCode = num => String.fromCharCode(num);
export const padEnd = (length, char) => str => sS(str).padEnd(length, char);
export const padStart = (length, char) => str => sS(str).padStart(length, char);
export const repeat = length => str => sS(str).repeat(length);
export const replace = (regexp, newStr) => str => sS(str).replace(regexp, newStr);
export const split = sep => str => sS(str).split(sep);
export const startsWith = term => str => sS(str).startsWith(term);
export const substring = (start, end) => str => sS(str).substring(start, end);
export const toLowerCase = str => sS(str).toLowerCase();
export const toUpperCase = str => sS(str).toUpperCase();
export const trim = str => sS(str).trim();
const sA = arr => (Array.isArray(arr) ? arr : []);
const ensureArray = a => (Array.isArray(a) ? a : exists(a) ? [a] : []);
export const array = (length = 0, mapper = id, filter = v => true) =>
new Array(length).fill(0).reduce((a, _, i) => a.concat(filter(i) ? mapper(i) : []), []);
export const concat = b => a => ensureArray(a).concat(ensureArray(b));
export const concatRight = a => b => ensureArray(a).concat(ensureArray(b));
export const every = func => arr => sA(arr).every(func);
export const filter = func => arr => sA(arr).filter(func);
export const find = func => arr => sA(arr).find(func);
export const findIndex = func => arr => sA(arr).findIndex(func);
export const forEach = (...funcs) => arr => sA(arr).forEach(Pipe(...funcs));
export const includes = thing => arr => sA(arr).includes(thing);
export const indexOf = term => arr => sA(arr).indexOf(term);
export const join = sep => arr => sA(arr).join(sep);
export const length = arr => sA(arr).length;
export const map = (...funcs) => arr => sA(arr).map(Pipe(...funcs));
export const reverse = arr => sA(arr).reverse();
export const slice = (begin, end) => arr => sA(arr).slice(begin, end);
export const some = func => arr => sA(arr).some(func);
export const sort = func => arr => sA(arr).sort(func);
export const sortBy = (path = "") =>
sort((a, b) => {
const A = get(path)(a);
const B = get(path)(b);
return lt(B)(A) ? -1 : gt(B)(A) ? 1 : 0;
});
const plainReduce = (func, initial) => arr =>
sA(arr).reduce((a, c) => func(c)(a), initial);
const mapFilterReduce = (reducer, initial, map, filter = v => true) => arr =>
sA(arr).reduce((a, c) => (filter(c) ? reducer(map(c))(a) : a), initial);
export const reduce = (reducer, initial, map, filter) =>
map ? mapFilterReduce(reducer, initial, map, filter) : plainReduce(reducer, initial);
export const int = n => parseInt(n);
export const float = n => parseFloat(n);
export const toFixed = dec => num => (num ? num.toFixed(dec) : num);
export const add = b => a => a + b;
export const divide = b => a => a / b;
export const multiply = b => a => a * b;
export const subtract = b => a => a - b;
export const max = arr => Math.max(...arr);
export const min = arr => Math.min(...arr);
export const clamp = (min, max) => n => Math.min(max, Math.max(min, n));
export const pow = exp => base => Math.pow(base, exp);
export const rangeMap = (inMin, inMax, outMin, outMax) => n =>
((n - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
export const assign = b => a => Object.assign({}, a, b);
export const has = (path = "") => obj => exists(get(path)(obj));
type Entry = [any?, any?];
export const objectFromEntry = ([k, v]: Entry = []) => (exists(k) ? { [k]: v } : {});
export const mapEntry = (mapKey, mapValue) => ([k, v]: Entry = []) => [
mapKey(k),
mapValue(v)
];
export const mapObject = (map, filter) => (obj = {}) =>
reduce(assign, {}, Pipe(map, objectFromEntry), filter)(Object.entries(obj));