Source on GitHub
Top Overview Performance Bonus Arguments Miscellanious

Overview๐Ÿ”—

fpx: Functional Programming eXtensions for JavaScript. Source on GitHub.

Lightweight replacement for Lodash, Underscore, etc. Differences:

Doesnโ€™t have complete feature parity with Lodash, and probably never will. More functions may be added on demand. Open a GitHub issue or a pull request if something useful is missing.

Written as an ES2015 module for compatibility with tree shaking. When building a browser bundle, Webpack 4+ or Rollup automatically pick the ES2015 version. In Node.js, you automatically get the CommonJS version. A properly configured bundler should strip out the unused code, leaving only what you actually use.

See sibling libraries:

Install from NPM. Current version: 0.7.0.

npm i -E fpx
# or
yarn add -E fpx

All examples on this page imply an import:

import * as f from 'fpx'
// or
const f = require('fpx')

On this page, Fpx is globally available as f or fpx. You can run the examples in the browser console.

Why๐Ÿ”—

Why a library: the built-ins are not sufficient. Fpx replaces some common code patterns with small functions, significantly reducing the code size. It also enables assertions that are desired but missing from most JS code.

Size๐Ÿ”—

Why not just use Lodash? Itโ€™s way, way, WAY too huge. You just want a few functions and BAM, you get โ‰ˆ 73 KiB minified. You could make a custom bundle, but most folks just import the whole thing. For a web developer, shipping so much useless code to your users is irresponsible. Itโ€™s also space-inefficient, bloated with avoidable code, and judging by the source, this seems unlikely to change. If you care about size, you need a replacement.

The current version of Lodash is incompatible with techniques like tree shaking / dead code elimination / live code inclusion, which pick just the functions you actually use, dropping the rest. These techniques work perfectly on Fpx. When using a module bundler that supports them, such as Rollup or Webpack 4+, you automatically get a โ€œcustom versionโ€ of Fpx without any unused stuff.

Simplicity๐Ÿ”—

Programs must be written for people to read, and only incidentally for machines to execute.

โ€”โ€‰Abelson & Sussman, โ€œStructure and Interpretation of Computer Programsโ€

I believe that all code should strive to be simple and educational. This gives me a massive distaste for most code ever written. For example, reading Lodashโ€™s source might teach you code obfuscation, but not much else.

In Fpx, I strive to keep the code and the algorithms dead simple, with as few unnecessary elements and indirections as possible. If you want to understand how this kind of library works, how higher-order functions work, how to manipulate JS data structures, Fpx should hopefully provide a good read.

Strictness๐Ÿ”—

Fpx functions tend to be somewhat stricter than their built-in counterparts, and much stricter than the Lodash counterparts. They tend to work either on lists (fold) or dicts (foldVals), not both. List functions also donโ€™t accept strings. This prevents subtle gotchas.

On the other hand, collection functions accept null and undefined, which is very useful in practice. This would not be possible with methods, since methods must be invoked on an object.

Unlike Lodash, higher-order functions always require an operator function. Thereโ€™s no implicit fallback on the identity function, and no implicit conversion of data patterns into functions.

Minifiable Assertions๐Ÿ”—

Assertions go a long way in debugging. Fail fast, catch bugs early. In asynchronous code, validating inputs as early as possible, instead of letting it fail mysteriously later, can save you hours of debugging.

Hereโ€™s the traditional way of doing assertions:

function someFunction(input) {
  if (typeof input !== 'function') throw Error(`Expected a function, got ${input}`)
}

someFunction({one: 10})
// Error: Expected a function, got [object Object]

Annoying to type and really bad for minification. Some folks strip assertions from production builds, but I find the idea flawed. Even in production, failing fast is better than failing mysteriously, and assertions help with debugging when it inevitably fails.

Fpx provides a much better alternative:

function someFunction(input) {
  f.validate(input, f.isFunction)
}

someFunction({one: 10})
// Error: Expected {"one":10} to satisfy test isFunction

So much better! Easy to type with editor autocompletion, produces good error messages, and minifies really well. In a minified build, the function name will be garbled, which I consider a good tradeoff.

To support this style of coding, Fpx provides validate and a bevy of boolean tests.


Performance๐Ÿ”—

For now, Fpx makes no bold performance claims, other than:

Thereโ€™s potential for improvement, but I donโ€™t have infinite spare time for microbenchmark contests. Suggestions are welcome.


Bonus Arguments๐Ÿ”—

In Fpx, all collection functions, such as map, pass up to 3 additional arguments to the operator function. Use this to define your functions statically and avoid local closures:

// local context
const a = 1
const b = 2
const c = 3


// bonus args (recommended)

function add5(value, key, a, b, c) {
  return value + key + a + b + c
}
f.map([10, 20, 30], add5, a, b, c)
// [16, 27, 38]


// closure (not recommended)

function add5(value, key) {
  return value + key + a + b + c
}
f.map([10, 20, 30], add5)
// [16, 27, 38]

Broadly speaking, closures have a cost; defining functions statically avoids that cost.

This doesnโ€™t always improve performance, and can even make it worse. A smart engine can sometimes optimize a closure away. Closures can accidentally enable optimizations like function specialization. However, you canโ€™t rely on such optimizations. As a rule of thumb, memory allocation beats all other costs. Avoiding closure allocation is more reliable and predictable at improving performance.

This may change with future advancements in JS engines.


Fun๐Ÿ”—

Miscellanious utilities and transforms for functions.


call(fun, ...args)๐Ÿ”—

Like Function.prototype.call. Sometimes useful with higher-order functions.

f.call(f.add, 10, 20)
// 3

// equivalent:
// f.add(10, 20)
// f.call(f.add, 10, 20)

// Side effect of implementation
// f.add.call(f.add, 10, 20)

apply(fun, args)๐Ÿ”—

Like Function.prototype.apply. Sometimes useful with higher-order functions.

f.apply(f.add, [10, 20])
// 3

// equivalent:
// f.add(10, 20)
// f.add(...[10, 20])
// f.apply(f.add, [10, 20])

// Side effect of implementation
// f.add.apply(f.add, [10, 20])

bind(fun, ...args)๐Ÿ”—

Like Function.prototype.bind, but sets implicit this = fun as a side effect of the implementation.

Returns a new function that represents partial application of the given function, a common tool in functional programming. When called, it joins arguments from both calls and invokes the original function. Think of it like splitting a function call in two, or more.

const inc = f.bind(f.add, 1)

inc(2)
// 3

not(fun)๐Ÿ”—

Returns a new function that negates the result of the given function, like a delayed !.

function eq(a, b) {return a === b}

const different = f.not(eq)

different(10, 20)
// !eq(10, 20) = true

// equivalent:
function different(a, b) {return !eq(a, b)}

Bool๐Ÿ”—

Boolean tests.


truthy(value)๐Ÿ”—

Aliases: truthy, bool.

Same as !! or Boolean. Sometimes useful with higher-order functions.

f.truthy(null)
// false

f.truthy(1)
// true

falsy(value)๐Ÿ”—

Aliases: falsy, negate.

Same as !. Sometimes useful with higher-order functions.

f.falsy(null)
// true

f.falsy(1)
// false

is(one, other)๐Ÿ”—

Identity test: same as ===, but considers NaN equal to NaN. Equivalent to SameValueZero as defined by the language spec.

Note that Object.is implements SameValue, which treats -0 and +0 as distinct values. This is typically undesirable. As a result, you should prefer f.is over === or Object.is.

Used internally in fpx for all identity tests.

f.is(1, '1')
// false

f.is(NaN, NaN)
// true

isNumber(value)๐Ÿ”—

Same as typeof value === 'number'. Returns true for NaN and ยฑInfinity. In most cases, you should use isFinite instead.

f.isNumber(1)
// true
f.isNumber('1')
// false
f.isNumber(NaN)
// true <-- WTF

isFinite(value)๐Ÿ”—

Same as ES2015โ€™s Number.isFinite.

Returns true if value is a number and is not NaN or ยฑInfinity. In most cases, you should use isFinite rather than isNumber.

f.isFinite(1)
// true
f.isFinite('1')
// false
f.isFinite(NaN)
// false

isInteger(value)๐Ÿ”—

True if value is an integer: finite without a fractional part.

f.isInteger(0)
// true
f.isInteger(1)
// true
f.isInteger(-1)
// true
f.isInteger(1.1)
// false
f.isInteger('1')
// false

isNatural(value)๐Ÿ”—

True if value is a natural number: a positive integer, starting with 0.

f.isNatural(0)
// true
f.isNatural(1)
// true
f.isNatural(-1)
// false
f.isNatural(1.1)
// false
f.isNatural('1')
// false

isNaN(value)๐Ÿ”—

Same as ES2015โ€™s Number.isNaN. True if value is actually NaN. Doesnโ€™t coerce non-numbers to numbers, unlike global.isNaN / window.isNaN.

f.isNaN(NaN)
// true
f.isNaN(undefined)
// false

isInfinity(value)๐Ÿ”—

True if value is -Infinity or Infinity.

f.isInfinity(Infinity)
// true
f.isInfinity(-Infinity)
// true
f.isInfinity(10)
// false
f.isInfinity(NaN)
// false
f.isInfinity(undefined)
// false

isString(value)๐Ÿ”—

f.isString('blah')
// true

isBoolean(value)๐Ÿ”—

f.isBoolean(false)
// true

isSymbol(value)๐Ÿ”—

f.isSymbol(Symbol('blah'))
// true

isKey(value)๐Ÿ”—

True if value could, with some suspension of disbelief, claim to be usable as a dict key. Must satisfy either of:

f.isKey('key')
// true
f.isKey(Symbol('key'))
// true
f.isKey(10)
// true
f.isKey(undefined)
// false

In other words, this is a subset of isPrimitive that excludes null, undefined, NaN, and ยฑInfinity. These values are often produced on accident, and you almost never want them as your dict keys.

Fpx uses isKey to validate keys in functions like keyBy.


isPrimitive(value)๐Ÿ”—

Opposite of isComplex. Either of:


isComplex(value)๐Ÿ”—

Definition:

function isComplex(value) {
  return isObject(value) || isFunction(value)
}

This covers all mutable objects in the true JavaScript sense, including functions.


isInstance(value, Class)๐Ÿ”—

Same as instanceof but more efficient for primitives.

When the left operand to instanceof is a primitive, it creates a temporary wrapper object, wasting CPU cycles on allocation and garbage collection, even though false was guaranteed. isInstance avoids this mistake. At the time of writing, the improvement is measurable in V8.

f.isInstance([], Array)          // true
f.isInstance(new Date(), Date)   // true
f.isInstance(1, Number)          // false, cheaper than instanceof
f.isInstance(undefined, Object)  // false, cheaper than instanceof

isFunction(value)๐Ÿ”—

f.isFunction(isFunction)
// true

isObject(value)๐Ÿ”—

True if value is a non-null object. This includes plain dicts, arrays, regexps, user-defined โ€œclassesโ€, built-in classes, and so on. Doesnโ€™t count functions as objects, even though technically they are.

Note: this is not equivalent to lodashโ€™s _.isObject, which counts functions as objects. See isComplex for that.

For plain objects used as dictionaries, see isDict. For fancy non-list objects, see isStruct.

f.isObject('blah')
// false

f.isObject(/blah/)
// true

f.isObject([])
// true

f.isObject(Object.create(null))
// true

f.isObject(() => {})
// false

isDict(value)๐Ÿ”—

True if value is a normal, honest-to-goodness dictionary and not something fancy-shmancy.

Roughly equivalent to Lodashโ€™s _.isPlainObject.

f.isDict({})
// true

f.isDict(Object.create(null))
// true

f.isDict(Object.create({}))
// false

f.isDict([])
// false

f.isDict(new class {}())
// false

isStruct(value)๐Ÿ”—

True if value is a non-list object. In Fpx lingo, such objects are called โ€œstructsโ€. Thereโ€™s an entire category of functions dedicated to them, similar to โ€œobjectโ€ functions in Lodash.

Note that anything that satisfies isDict automatically satisfies isStruct, but not vice versa.

f.isStruct({})
// true

f.isStruct(new RegExp())
// true

f.isStruct([])
// false

f.isStruct(f.isStruct)
// false

isArray(value)๐Ÿ”—

True if value inherits from Array.prototype.

f.isArray([])
// true

isList(value)๐Ÿ”—

True if value looks array-like, such as:

Used internally for most list checks. Note that strings are not considered lists.

f.isList([])
// true

function args() {return arguments}
f.isList(args())
// true

f.isList(new Uint8Array())
// true

f.isList(document.querySelectorAll('div'))
// true

f.isList('string')
// false

isRegExp(value)๐Ÿ”—

f.isRegExp(/blah/)
// true

isDate(value)๐Ÿ”—

f.isDate(new Date())             // true
f.isDate(new Date().toString())  // false

isValidDate(value)๐Ÿ”—

f.isDate(new Date())     // true
f.isDate(new Date(NaN))  // false

isInvalidDate(value)๐Ÿ”—

f.isDate(new Date())     // false
f.isDate(new Date(NaN))  // true

isPromise(value)๐Ÿ”—

True if the value quacks like an ES2015 Promise. Not limited to built-in promises.

f.isPromise(new Promise(() => {}))
// true

f.isPromise({then() {}, catch() {}})
// true

f.isPromise({then() {}})
// false

isIterator(value)๐Ÿ”—

True if the value quacks like an ES2015 iterator. Iterators, also called generator objects, are created by calling a generator function.

function* myGenerator() {
  yield 10
  yield 20
  return 30
}

f.isIterator(myGenerator())
// true

f.isIterator(myGenerator)
// false

isNil(value)๐Ÿ”—

True for null and undefined. Same as value == null.

Incidentally, these are the only values that produce an exception when attempting to read a property: null.someProperty.

// Definition
function isNil(value) {return value == null}

f.isNil(null)
// true

f.isNil(undefined)
// true

f.isNil(false)
// false

isSomething(value)๐Ÿ”—

True for everything except null and undefined.

// Definition
function isSomething(value) {return value != null}

f.isSomething(null)
// false
f.isSomething(undefined)
// false
f.isSomething(false)
// true

isEmpty(value)๐Ÿ”—

True if !size(value). Only lists and dicts can be non-empty.

f.isEmpty(undefined)
// true

f.isEmpty('blah')
// true

f.isEmpty([10, 20])
// false

f.isEmpty({one: 10, two: 20})
// false

testBy(value, pattern)๐Ÿ”—

Limited form of pattern testing. Together with ES2015 destructuring, it lets you crudely approximate pattern matching, a feature common in functional languages but missing from JavaScript.

Tests value against pattern, using the following rules:

// Function pattern: call it, convert result to boolean

f.testBy(10, f.inc)  โ‰ก  !!f.inc(10)

// Primitive pattern: test for identity via `f.is`

f.testBy(x, null)  โ‰ก  f.is(x, null)
f.testBy(x, 10)    โ‰ก  f.is(x, 10)
f.testBy(x, NaN)   โ‰ก  f.is(x, NaN)

// Regexp pattern:
//   input must be a string
//   use `RegExp.prototype.test`

f.testBy(x, /blah/)  โ‰ก  f.isString(x) && /blah/.test(x)

// List pattern:
//   input must be a list
//   recursively apply sub-patterns

f.testBy(x, [])             โ‰ก  f.isList(x)
f.testBy(x, [/blah/])       โ‰ก  f.isList(x) && f.testBy(x[0], /blah/)
f.testBy(x, [/blah/, 'c'])  โ‰ก  f.isList(x) && f.testBy(x[0], /blah/) && f.testBy(x[1], 'c')

// Struct pattern:
//   input must a struct (a non-list object)
//   recursively apply sub-patterns

f.testBy(x, {})             โ‰ก  f.isStruct(x)
f.testBy(x, {one: /blah/})  โ‰ก  f.isStruct(x) && f.testBy(x.one, /blah/)
f.testBy(x, {a: {b: 'c'}})  โ‰ก  f.isStruct(x) && f.testBy(x.a, {b: 'c'})

test(pattern)๐Ÿ”—

Takes a pattern and returns a version of testBy bound to that pattern. See the rules above.

f.test(pattern)
// โ‰ก function(x) {return f.testBy(x, pattern)}

f.test(pattern)(input)
// โ‰ก f.testBy(input, pattern)

Casts๐Ÿ”—

Type coercions and replacements.

onlyString(value)๐Ÿ”—

Nil-tolerant string assertion. Replaces null or undefined with '', otherwise asserts isString and returns value.

f.onlyString()
// ''

f.onlyString('blah')
// 'blah'

f.onlyString(['not string'])
// Error: Expected ["not string"] to satisfy test isString

onlyList(value)๐Ÿ”—

Nil-tolerant list assertion. Replaces null or undefined with [], otherwise asserts isList and returns value. Used internally in list functions.

f.onlyList()
// []

f.onlyList([10, 20])
// [10, 20]

f.onlyList('not list')
// Error: Expected "not list" to satisfy test isList

onlyDict(value)๐Ÿ”—

Nil-tolerant dict assertion. Replaces null or undefined with {}, otherwise asserts isDict and returns value.

f.onlyDict()
// {}

f.onlyDict({one: 10})
// {one: 10}

f.onlyDict('not dict')
// Error: Expected "not dict" to satisfy test isDict

onlyStruct(value)๐Ÿ”—

Nil-tolerant struct assertion. Replaces null or undefined with {}, otherwise asserts isStruct and returns value. Used internally in struct functions.

f.onlyStruct()
// {}

f.onlyStruct({one: 10})
// {one: 10}

f.onlyStruct('not struct')
// Error: Expected "not struct" to satisfy test isDict

toArray(value)๐Ÿ”—

Returns value when isArray. If value is a non-array list, converts it to an Array. Otherwise replaces with []. Sometimes useful for converting arguments or other non-array lists to arrays.

f.toArray([10, 20])
// [10, 20]

f.toArray()
// []

f.toArray(function args() {}(10, 20))
// [10, 20]

List๐Ÿ”—

List manipulation utils.

Common rules:

Note that strings are not considered lists.


each(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Like Array.prototype.forEach, but works on null, undefined, and array-likes.

function report(value, index, a, b, c) {
  console.info(value, index, a, b, c)
}

f.each(['one', 'two'], report, 10, 20, 30)
// 'one' 0 10 20 30
// 'two' 1 10 20 30

fold(list, init, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(accumulator, value, index, a, b, c)

Like Array.prototype.reduce, with the following differences:

f.fold([10, 20], 5, f.add)
// 5 + 10 + 20 = 35

foldRight(list, init, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(accumulator, value, index, a, b, c)

Like Array.prototype.reduceRight, with the following differences:

f.foldRight([1, 5, 20], 100, f.sub)
// 100 - 20 - 5 - 1 = 74

map(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Like Array.prototype.map, but works on null, undefined, and array-likes.

function double(num) {return num * 2}

f.map([10, 20, 30], double)
// [20, 40, 60]

Note: coming from Lodash, you might miss the string shortcut:

_.map([{value: 10}, {value: 20}], 'value')
// [10, 20]

Fpx considers this a hazardous malpractice. Just use a function:

f.map([{value: 10}, {value: 20}], x => x.value)

flatMap(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Similar to map, but flattens any lists returned by fun into the output array. Equivalent to flatten(map(...arguments)).

f.flatMap([10, [20], [[30]]], x => x)
// [10, 20, [30]]

flatMapDeep(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Similar to map, but deeply flattens any lists returned by fun, returning a completely flat array. Equivalent to flattenDeep(map(...arguments)).

f.flatMapDeep([10, [20], [[[30]]]], x => x)
// [10, 20, 30]

mapFilter(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Similar to map, but drops any โ€œfalsyโ€ values from the output. Equivalent to compact(map(...arguments)).

f.mapFilter([10, 0, 20, 0], x => x * 2)
// [20, 40]

filter(list, test, a, b, c)๐Ÿ”—

where test: ฦ’(value, index, a, b, c)

Like Array.prototype.filter, but works on null, undefined, and array-likes.

f.filter([10, 20, true, false], f.isBoolean)
// [true, false]

Note: coming from Lodash, you might miss the magic pattern shortcut:

_.filter([{val: 10}, {val: 20}], {val: 10})
// [{val: 10}]

Fpx provides test:

f.filter([{val: 10}, {val: 20}], f.test({val: 10}))
// [{val: 10}]

Be wary that itโ€™s slower than a hand-coded test.


reject(list, test, a, b, c)๐Ÿ”—

where test: ฦ’(value, index, a, b, c)

Opposite of filter: drops elements that donโ€™t satisfy test.

f.reject([10, 20, true, false], f.isNumber)
// [true, false]

compact(list)๐Ÿ”—

Returns a version of list without any โ€œfalsyโ€ elements. Equivalent to filter(list, id).

f.compact([10, 0, 20, NaN, 30, undefined])
// [10, 20, 30]

find(list, test, a, b, c)๐Ÿ”—

where test: ฦ’(value, index, a, b, c)

Like Array.prototype.find, but works on null, undefined, and array-likes.

f.find([10, true, 20, false, 30], f.isBoolean)
// true

findRight(list, test, a, b, c)๐Ÿ”—

where test: ฦ’(value, index, a, b, c)

Like find, but iterates from the end of the list. Returns the rightmost element that satisfies test, or undefined if none do.

f.findRight([10, true, 20, false, 30], f.isBoolean)
// false

findIndex(list, test, a, b, c)๐Ÿ”—

where test: ฦ’(value, index, a, b, c)

Like Array.prototype.findIndex, but works on null, undefined, and array-likes.

f.findIndex([10, true, 20, false, 30], f.isBoolean)
// 1

findIndexRight(list, test, a, b, c)๐Ÿ”—

where test: ฦ’(value, index, a, b, c)

Like findIndex, but iterates from the end of the list. Returns the index of the rightmost element that satisfies test, or -1 if none do.

f.findIndexRight([10, true, 20, false, 30], f.isBoolean)
// 3

indexOf(list, value)๐Ÿ”—

Like Array.prototype.indexOf, with the following differences:

f.indexOf([10, NaN, NaN, 20], NaN)
// 1

lastIndexOf(list, value)๐Ÿ”—

Like Array.prototype.lastIndexOf, with the following differences:

f.lastIndexOf([10, NaN, NaN, 20], NaN)
// 2

includes(list, value)๐Ÿ”—

Like Array.prototype.includes, but works on null, undefined, and array-likes.

f.includes([10, 20, 30], NaN)
// false

f.includes([10, 20, NaN], NaN)
// true

procure(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Similar to find, but returns the first truthy result of calling fun, rather than the corresponding list element.

function double(num) {return num * 2}

f.procure([0, 0, 10, 100], double)
// double(10) = 20

every(list, test, a, b, c)๐Ÿ”—

where test: ฦ’(value, index, a, b, c)

Like Array.prototype.every, but works on null, undefined, and array-likes.

f.every([], f.isBoolean)
// true

f.every([true, false], f.isBoolean)
// true

f.every([true, false, 10, 20], f.isBoolean)
// false

some(list, test, a, b, c)๐Ÿ”—

where test: ฦ’(value, index, a, b, c)

Like Array.prototype.some, but works on null, undefined, and array-likes.

f.some([], f.isBoolean)
// false

f.some([10, 20], f.isBoolean)
// false

f.some([true, false, 10, 20], f.isBoolean)
// true

slice(list, start, end)๐Ÿ”—

Like Array.prototype.slice, but also accepts null and undefined. start and end can be missing or negative; see the linked documentation.

f.slice([10, 20, 30, 40, 50], 1, -1)
// [20, 30, 40]

append(list, value)๐Ÿ”—

Returns a version of list with value appended at the end.

f.append([10, 20], 30)
// [10, 20, 30]

prepend(list, value)๐Ÿ”—

Returns a version of list with value prepended at the start.

f.prepend([20, 30], 10)
// [10, 20, 30]

remove(list, value)๐Ÿ”—

Returns a version of list with one occurrence of value removed. May return the original list.

f.remove(['one', 'two', 'three'], 'two')
// ['one', 'three']

f.remove(['one', 'two', 'one'], 'one')
// ['two', 'one']

insertAtIndex(list, index, value)๐Ÿ”—

Returns a version of list with value inserted at index, moving subsequent elements to the end. index must be an integer within list bounds + 1, otherwise throws.

f.insertAtIndex(undefined, 0, 'zero')
// ['zero']

f.insertAtIndex(['zero', 'one', 'two'], 0, 'absolute zero')
// ['absolute zero', 'zero', 'one', 'two']

f.insertAtIndex(['zero', 'one', 'two'], 2, '...')
// ['zero', 'one', '...', 'two']

removeAtIndex(list, index)๐Ÿ”—

Returns a version of list with the value at index removed, if within bounds. index must be an integer, otherwise throws.

f.removeAtIndex(['zero', 'one', 'two'], 0)
// ['one', 'two']

f.removeAtIndex(['zero', 'one', 'two'], 1)
// ['zero', 'two']

f.removeAtIndex(['zero', 'one', 'two'], 10)
// ['zero', 'one', 'two']

adjoin(list, value)๐Ÿ”—

Appends value to list, duplicate-free. Returns the same list if it already includes value. Always returns an Array, converting the input from a non-array list.

f.adjoin([10, 20], 30)
// [10, 20, 30]

f.adjoin([10, 20, 30], 20)
// [10, 20, 30]

toggle(list, value)๐Ÿ”—

Appends or removes value, depending on whether itโ€™s already included.

f.toggle([10, 20], 30)
// [10, 20, 30]

f.toggle([10, 20, 30], 30)
// [10, 20]

concat(...lists)๐Ÿ”—

Concatenates lists, ignoring non-list arguments.

Different from Array.prototype.concat and, by extension, lodashโ€™s _.concat. They inherited Schemeโ€™s hazardous mistake of appending non-list inputs while flattening list inputs. This leads to surprising errors and/or intentional abuse. fpxโ€˜s concat rejects non-lists, preventing this gotcha.

Note: for individual elements, use append and prepend instead.

f.concat()
// []

f.concat([10], [20], [30])
// [10, 20, 30]

f.concat([10, 20], 30)
// Error: Expected 30 to satisfy test isList

flatten(list)๐Ÿ”—

Returns a version of list flattened one level down.

f.flatten([10, [20], [[30]]])
// [10, 20, [30]]

flattenDeep(list)๐Ÿ”—

Returns a version of list with all nested lists flattened into one result.

f.flattenDeep([10, [20], [[[30]]]])
// [10, 20, 30]

head(list)๐Ÿ”—

Returns the first element of the given list.

f.head()
// undefined

f.head([10, 20, 30])
// 10

tail(list)๐Ÿ”—

Returns all but first element of the given list.

f.tail()
// []

f.tail([10, 20, 30])
// [20, 30]

init(list)๐Ÿ”—

Returns all but last element of the given list.

f.init()
// []

f.init([10, 20, 30])
// [10, 20]

last(list)๐Ÿ”—

Returns the last element of the given list.

f.last()
// undefined

f.last([10, 20, 30])
// 30

take(list, count)๐Ÿ”—

Returns a sub-list with count elements taken from the start. Equivalent to slice(list, count).

f.take(undefined, 0)
// []

f.take([10, 20, 30, 40], 2)
// [10, 20]

f.take([10, 20, 30, 40], Infinity)
// [10, 20, 30, 40]

drop(list, count)๐Ÿ”—

Returns a sub-list with count elements removed from the start. Equivalent to slice(list, 0, count).

f.drop(undefined, 0)
// []

f.drop([10, 20, 30, 40], 2)
// [30, 40]

f.drop([10, 20, 30, 40], Infinity)
// []

reverse(list)๐Ÿ”—

Like Array.prototype.reverse, with the following differences:

f.reverse()
// []

f.reverse([10, 20, 30])
// [30, 20, 10]

sort(list, comparator)๐Ÿ”—

Like Array.prototype.sort, with the following differences:

// Messed-up default JS sorting
f.sort([3, 22, 111])
// [111, 22, 3]

// Use a custom comparator to sort numbers properly
f.sort([3, 22, 111], f.sub)
// [3, 22, 111]

sortBy(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, a, b, c)

Returns a version of list sorted by the order of values returned by fun, which is called on every element with the bonus arguments. Kinda like mapping list to fun, sorting the result, then changing the element order in the original list the same way.

The โ€œvirtual elementsโ€ are sorted the same way as in .sort(), i.e. by the Unicode code order of its stringified elements; see the relevant part of the spec. sortBy currently doesnโ€™t accept a custom comparator, although this could be changed if needed.

Works on array-likes. Note that fun doesnโ€™t receive an element index.

function getId({id}) {return id}

f.sortBy([{id: 3}, {id: 22}, {id: 111}], getId)
// [{id: 111}, {id: 22}, {id: 3}]

intersection(left, right)๐Ÿ”—

Returns a list representing a set intersection of the two lists. It contains only the elements that occur in both lists, tested via is, without any duplicates.

f.intersection([10, 20, 20, 30], [20, 30, 30, 40])
// [20, 30]

f.intersection([10, 20], undefined)
// []

keyBy(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Returns a dict where listโ€˜s values are assigned to the keys created by fun.

Major difference from Lodashโ€™s _.keyBy: keys must pass the isKey test or be ignored. This means they must be primitives, excluding the nonsense values null, undefined, NaN and ยฑInfinity. This helps avoid accidental garbage in the output.

function double(value) {return value * 2}

f.keyBy([10, 20, 30], double)
// {20: 10, 40: 20, 60: 30}

groupBy(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Similar to keyBy: returns a dict where keys have been created by calling fun. Unlike keyBy, it groups values into lists, accumulating them for repeating keys instead of overwriting.

Just like keyBy, and unlike Lodashโ€™s _.groupBy, keys must pass the isKey test or be ignored. This helps avoid accidental garbage in the output.

function oddness(value) {return value % 2}

f.groupBy([10, 13, 16, 19], oddness)
// {0: [10, 16], 1: [13, 19]}

uniq(list)๐Ÿ”—

Returns a version of list without duplicate elements, compared via is.

f.uniq([10, 20, NaN, 20, NaN, 30])
// [10, 20, NaN, 30]

uniqBy(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Returns a version of list where no two elements have produced the same result when fun was called on them. The results are compared via is.

function isOdd(value) {return Boolean(value % 2)}

f.uniqBy([10, 13, 16, 19], isOdd)
// [10, 13]

partition(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Splits list into โ€œacceptedโ€ and โ€œrejectedโ€ groups. The accepted group contains elements for which fun returned something truthy, and the rejected group contains the rest.

function isOdd(value) {return Boolean(value % 2)}

f.partition([10, 13, 16, 19], isOdd)
// [[13, 19], [10, 16]]

sum(list)๐Ÿ”—

Sums all elements of list that satisfy isFinite, ignoring the rest.

f.sum([10, NaN, 20, '5'])
// 30

sumBy(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Calls fun on every element of the list and sums the results. Like sum, ignores values that donโ€™t satisfy isFinite.

f.sumBy([10, undefined, '20'], Number)
// 30

min(list)๐Ÿ”—

Finds the smallest value in list that also satisfies isFinite, or undefined. Note that it ignores ยฑInfinity.

f.min([])
// undefined

f.min(['10', 20, '30', -Infinity, NaN])
// 20

max(list)๐Ÿ”—

Finds the largest value in list that also satisfies isFinite, or undefined. Note that it ignores ยฑInfinity.

f.max([])
// undefined

f.max(['10', 20, '30', Infinity, NaN])
// 20

minBy(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Calls fun on every element of the list and returns the smallest result, using the same rules as min.

Note a major difference from Lodashโ€™s _.minBy: this returns the smallest value returned by fun, not its corresponding list element. I find this far more intuitive. See findMinBy for the counterpart to _.minBy.

function getNum({num}) {return num}

f.minBy([{num: 10}, {num: 20}, {num: 30}], getNum)
// 10

maxBy(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Calls fun on every element of the list and returns the largest result, using the same rules as max.

Note a major difference from Lodashโ€™s _.maxBy: this returns the smallest value returned by fun, not its corresponding list element. I find this far more intuitive. See findMaxBy for the counterpart to _.maxBy.

function getNum({num}) {return num}

f.maxBy([{num: 10}, {num: 20}, {num: 30}], getNum)
// 30

findMinBy(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Calls fun on every element of the list and returns the element for which fun returned the smallest value, using the same rules as min.

Similar to Lodashโ€™s _.minBy.

function getNum({num}) {return num}

f.findMinBy([{num: 10}, {num: 20}, {num: 30}], getNum)
// {num: 10}

findMaxBy(list, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, index, a, b, c)

Calls fun on every element of the list and returns the element for which fun returned the largest value, using the same rules as max.

Similar to Lodashโ€™s _.maxBy.

function getNum({num}) {return num}

f.findMaxBy([{num: 10}, {num: 20}, {num: 30}], getNum)
// {num: 30}

range(start, end)๐Ÿ”—

Returns a list of integers from start (inclusive) to end (exclusive). Both inputs must be natural numbers, with start <= end.

f.range(5, 10)
// [5, 6, 7, 8, 9]

Struct๐Ÿ”—

Utils for dealing with non-list objects, called โ€œstructsโ€ or โ€œdictionariesโ€ in Fpx.

Common rules:

Getter functions like get accept any input, ignoring non-objects.

Iteration functions accept null, undefined, and non-list objects, rejecting any other input with an exception.


get(value, key)๐Ÿ”—

Same as value[key], but safe on null or undefined.

f.get()
// undefined

f.get(null, 'one')
// undefined

f.get({one: 1}, 'one')
// 1

f.get('string', 'length')
// 6

scan(value, ...path)๐Ÿ”—

Like get but takes many keys and reads a nested property at that path. Like get, is safe against null or undefined.

f.scan()
// undefined

f.scan(null)
// null

f.scan(null, 'one')
// undefined

f.scan({one: 1}, 'one')
// 1

f.scan({one: {two: 2}}, 'one', 'two')
// 2

getIn(value, path)๐Ÿ”—

Like scan but expects the entire path as the second argument. Like get, is safe against null or undefined.

f.getIn(1, [])
// 1

f.getIn({one: {two: 2}}, ['one', 'two'])
// 2

getter(key)๐Ÿ”—

Delayed get. Equivalent to value => get(value, key).

f.map([{value: 10}, {value: 20}], f.getter('value'))
// [10, 20]

Convenient, but also a performance malpractice. The โ€œrightโ€ way, performance-wise, is to statically define a getter function with a hardcoded property:

function getValue({value}) {return value}

f.map([{value: 10}, {value: 20}], getValue)
// [10, 20]

keys(dict)๐Ÿ”—

Like Object.keys, with the following differences:

f.keys()
// []

f.keys({one: 10, two: 20})
// ['one', 'two']

f.keys([10, 20])
// Error: Expected [10,20] to satisfy test isStruct

values(dict)๐Ÿ”—

Like Object.values, with the following differences:

f.values()
// []

f.values({one: 10, two: 20})
// [10, 20]

f.values([10, 20])
// Error: Expected [10,20] to satisfy test isStruct

entries(dict)๐Ÿ”—

Like Object.entries, with the following differences:

f.entries()
// []

f.entries({one: 10, two: 20})
// [['one', 10], ['two', 20]]

f.entries([10, 20])
// Error: Expected [10,20] to satisfy test isStruct

eachVal(dict, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, key, a, b, c)

Iterates for side effects, calling fun with every property and key. Returns undefined.

function report(value, key, a, b, c) {
  console.info(value, key, a, b, c)
}

f.eachVal({one: 10, two: 20}, report, 10, 20, 30)
// 10 'one' 10 20 30
// 20 'two' 10 20 30

foldVals(dict, init, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(accumulator, value, key, a, b, c)

Similar to fold, but for dicts. Iterates over each property, updating the accumulator, which is returned in the end.

f.foldVals({one: 10, two: 20}, 5, f.add)
// 5 + 10 + 20 = 35

mapVals(dict, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, key, a, b, c)

Similar to map, but for dicts. Creates a version of dict where values have been replaced by calling fun.

function bang(value) {return value + '!'}

f.mapVals({ping: 'ping', pong: 'pong'}, bang)
// {ping: 'ping!', pong: 'pong!'}

mapKeys(dict, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(key, value, a, b, c)

Similar to mapVals, but replaces keys rather than values.

Major difference from Lodashโ€™s _.mapKeys: keys must pass the isKey test or be ignored. This means they must be primitives, excluding the nonsense values null, undefined, NaN and ยฑInfinity. This helps avoid accidental garbage in the output.

Another major difference from Lodashโ€™s _.mapKeys: the operator receives key, value, a, b, c rather than value, key, dict.

f.mapKeys({one: 10, two: 20}, f.head)
// {o: 10, t: 20}

mapValsSort(dict, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, key, a, b, c)

Maps dict to a list, sorted by key order.

Note the difference: Lodashโ€™s _.map works on dicts, but since object key/iteration order is unspecified, the output is unsorted and therefore unstable. mapValsSort avoids this issue, always producing the same output for a given dict.

f.mapValsSort({3: 'three', 22: 'two', 111: 'one'}, f.id)
// ['one', 'two', 'three']

pickBy(dict, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, key, a, b, c)

Similar to filter, but for dicts. Returns a version of dict with properties for which fun returned something truthy.

function isOdd(value) {return Boolean(value % 2)}

f.pickBy({one: 10, two: 13, three: 16, four: 19}, isOdd)
// {two: 13, four: 19}

omitBy(dict, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, key, a, b, c)

Similar to reject, but for dicts. Returns a version of dict without properties for which fun returned something truthy.

function isOdd(value) {return Boolean(value % 2)}

f.omitBy({one: 10, two: 13, three: 16, four: 19}, isOdd)
// {one: 10, three: 16}

pickKeys(dict, keys)๐Ÿ”—

Returns a version of dict with only the properties whitelisted in keys. The keys must satisfy isKey.

Same as Lodashโ€™s _.pick.

f.pickKeys({one: 10, two: 20}, ['one'])
// {one: 10}

omitKeys(dict, keys)๐Ÿ”—

Returns a version of dict without any properties blacklisted in keys. The keys must satisfy isKey.

Same as Lodashโ€™s _.omit.

f.omitKeys({one: 10, two: 20}, ['one'])
// {two: 20}

findVal(dict, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, key, a, b, c)

Similar to find, but for dicts. Returns the first value for which fun returned something truthy.

function isOdd(value) {return Boolean(value % 2)}

f.findVal({one: 10, two: 13}, isOdd)
// 13

findKey(dict, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, key, a, b, c)

Similar to findIndex, but for dicts. Returns the first key for which fun returned something truthy.

function isOdd(value) {return Boolean(value % 2)}

f.findKey({one: 10, two: 13}, isOdd)
// 'two'

invert(dict)๐Ÿ”—

Returns a version of dict with keys and values swapped. Values must satisfy isKey to become keys; ones that donโ€™t are silently dropped from the output.

f.invert({one: 10, two: 20})
// {10: 'one', 20: 'two'}

invertBy(dict, fun, a, b, c)๐Ÿ”—

where fun: ฦ’(value, key, a, b, c)

Similar to invert, but calls fun on each value to produce a key. The resulting keys must satisfy isKey or be silently dropped from the output.

function double(value) {return value * 2}

f.invertBy({one: 10, two: 20}, double)
// {20: 'one', 40: 'two'}

Coll๐Ÿ”—

Functions that work on both lists and dicts.


size(value)๐Ÿ”—

Depends on valueโ€˜s type:

f.size([10, 20])
// 2

f.size({one: 10, two: 20})
// 2

f.size()
// 0

f.size(f.size)
// 0

Also see isEmpty for a pure boolean version.


vacate(value)๐Ÿ”—

If size(value) > 0, returns value unchanged, otherwise returns undefined.

f.vacate([])
// undefined

f.vacate([10, 20])
// [10, 20]

f.vacate({})
// undefined

f.vacate({one: 10, two: 20})
// {one: 10, two: 20}

Ops๐Ÿ”—

Operator-style functions. Sometimes useful with higher-order functions. Like with regular JS operators, beware of implicit type coercions.


add(a, b)๐Ÿ”—

Same as +.

f.add(10, 20)
// 10 + 20 = 30

sub(a, b)๐Ÿ”—

Same as -.

f.sub(20, 10)
// 20 - 10 = 10

mul(a, b)๐Ÿ”—

Same as *.

f.mul(10, 20)
// 10 * 20 = 200

div(a, b)๐Ÿ”—

Same as /.

f.div(10, 20)
// 10 / 20 = 0.5

rem(a, b)๐Ÿ”—

Same as %.

f.rem(2.5, 1)
// 2.5 % 1 = 0.5

lt(a, b)๐Ÿ”—

Same as <.

f.lt(10, 20)
// 10 < 20 = true

gt(a, b)๐Ÿ”—

Same as >.

f.gt(10, 20)
// 10 > 20 = false

lte(a, b)๐Ÿ”—

Same as <=.

f.lte(10, 20)
// 10 <= 20 = true
f.lte(10, 10)
// 10 <= 10 = true

gte(a, b)๐Ÿ”—

Same as >=.

f.gte(10, 20)
// 10 >= 20 = false
f.gte(10, 10)
// 10 >= 10 = true

inc(num)๐Ÿ”—

Increments by 1.

f.inc(1)
// 1 + 1 = 2

dec(num)๐Ÿ”—

Decrements by 1.

f.dec(2)
// 2 - 1 = 1

Misc๐Ÿ”—

Uncategorised utils.


global๐Ÿ”—

The global object for the current environment:


id(value)๐Ÿ”—

Identity function: returns its first argument unchanged. Sometimes useful with higher-order functions.

f.id('first', 'second', 'third')
// 'first'

di(_, value)๐Ÿ”—

Returns its second argument unchanged. Sometimes useful with higher-order functions.

f.di('first', 'second', 'third')
// 'second'

val(value)๐Ÿ”—

โ€œConstantโ€ function. Returns a function that always returns the original value. Useful for dealing with functional APIs when values are known in advance.

const one = f.val(1)

one()
// 1

one(100)
// 1

noop๐Ÿ”—

Empty function. Functional equivalent of ; or undefined. Sometimes useful with higher-order functions.

f.noop()
// undefined

rethrow(value)๐Ÿ”—

Same as throw but can be used as an expression. Also sometimes useful with higher-order functions.

// Can be used where the regular `throw` can't
const x = someTest ? someValue : f.rethrow(Error('unreachable'))

assign(target, ...sources)๐Ÿ”—

Like Object.assign, but stricter:

const target = {}
f.assign(target, {one: 10}, {two: 20})
target
// {one: 10, two: 20}

maskBy(value, pattern)๐Ÿ”—

Overlays pattern on value, using the following rules:

// Function pattern: call as-is

f.maskBy(x, f.inc)  โ‰ก  f.inc(x)

// Primitive pattern: replace input

f.maskBy(x, null)   โ‰ก  null
f.maskBy(x, 10)     โ‰ก  10
f.maskBy(x, NaN)    โ‰ก  NaN

// Regexp pattern:
//   input must be nil or string
//   call `.match(regexp)`

f.maskBy(x, /blah/)  โ‰ก  f.onlyString(x).match(/blah/)

// List pattern:
//   input must be nil or list
//   recursively apply sub-patterns

f.maskBy(x, [])             โ‰ก  f.onlyList(x)
f.maskBy(x, [/blah/])       โ‰ก  [f.maskBy(x[0], /blah/)]
f.maskBy(x, [/blah/, 'c'])  โ‰ก  [f.maskBy(x[0], /blah/), f.maskBy(x[1], 'c')]

// Struct pattern:
//   input must be nil or struct
//   recursively apply sub-patterns

f.maskBy(x, {})             โ‰ก  f.onlyStruct(x)
f.maskBy(x, {one: /blah/})  โ‰ก  {one: f.maskBy(x.one, /blah/)}
f.maskBy(x, {a: {b: 'c'}})  โ‰ก  {a: f.maskBy(x.a, {b: 'c'})}

mask(pattern)๐Ÿ”—

Takes a pattern and returns a version of maskBy bound to that pattern. See the rules above.

f.mask(pattern)
// โ‰ก function(x) {return f.maskBy(x, pattern)}

f.mask(pattern)(input)
// โ‰ก f.maskBy(input, pattern)

validate(value, test)๐Ÿ”—

where test: ฦ’(value) -> boolean

Minification-friendly assertion. If !test(value), throws an exception with a message including value and the name of the test function.

Since the assertion doesnโ€™t contain any strings, it can be minified to just a few characters.

f.validate({}, f.isObject)
//

f.validate('blah', f.isFunction)
// Uncaught Error: Expected blah to satisfy test isFunction

validateEach(list, test)๐Ÿ”—

Same as validate but asserts each value in the provided list. Includes the list index in the error message.

f.validateEach([f.isFunction], f.isFunction)
//

f.validateEach(['blah'], f.isFunction)
// Uncaught Error: Expected blah at index 0 to satisfy test isFunction

validateInstance(object, Class)๐Ÿ”—

Asserts that object is an instance of the given class. Useful for writing ES5-style classes.

function Queue() {
  f.validateInstance(this, Queue)
}

show(value)๐Ÿ”—

Returns a string describing the value. Prints plain data as JSON to avoid the dreaded [object Object]. Prints functions as their names or source code. Convenient for interpolating things into error messages. Used internally in validate.

f.show(10)
// '10'

f.show(f.show)
// 'show'

f.show({one: 10, two: 20})
// '{"one":10,"two":20}'

Miscellanious๐Ÿ”—

Iโ€™m receptive to suggestions. If this library almost satisfies you but needs changes, open an issue on GitHub or chat me up. Contacts: https://mitranim.com/#contacts.