Succor—pure, fast, small utility library
npm install @antediluvian/succor
Looking for the API reference documentation?
Table of contents
There is not exactly a shortage of utility libraries for JavaScript, so what could possibly incite a perceived need to create another one? I don't have a great excuse beyond wanting both more and less than what I've found.
An ideal utility library SHOULD
- Be as fast as hand-crafted idiomatic code doing the same thing. Otherwise you have to consider whether you're in a performance critical part of your code or not before reaching for it, which makes it less generally useful.
- Work with the native data structures that ship with Node.js. I do favor immutable data structures, but that's just not what the language has, and a general utility library shouldn't require picking a specific data structure library.
- Work with the code you already have without major restructuring. If you need to do a whole-app refactoring it's hardly a utility library anymore.
- Focus on the utility functions that matter the most. There is an unbounded number of possible utility functions one could conjure up, but from what I've seen in the wild I would never find a use for the vast majority of them.
An ideal utility library SHOULD NOT
- Mutate any of its arguments. I've seen my share of bugs happening from mutated arguments and I don't fancy seeing any more of them than I have to.
- Throw errors because of undefined/null values. If you've done your validation/parsing properly up front this shouldn't be a concern. But if you haven't you probably won't be in a great position by your utility function throwing errors. At the very least you should be able to rely on your utility library either always or never throwing errors. Any mix of the two is a recipe for bugs.
- Do the whole monad thing. I don't want to pretend that Node.js has built-in Option, Result, or other types. They are mostly valuable tools at compile time. I have not seen their value at runtime.
Prior art
This section summarizes why I can't just use any of the existing popular utility libraries.
Why not lodash?
- Some of the functions mutate their arguments and I can't remember which until something mutates at runtime in production.
- It is built in a very curry-unfriendly way, which leads to repeatedly having to name identifiers all over the place.
- There are enough functions to always have you wondering if you could possibly wrangle your use case to be “lodash'ed”.
- It's slow. Faster than the others on the list, but an order of magnitude slower than hand-crafted code.
Who not lodash/fp?
- I just can not for the life of me figure out how to use it. The documentation lists a bunch of rules for how the regular lodash functions are translated into their lodash/fp variants, but there's no easy reference documentation for what results from it. The whole thing looks like a half-baked experiment to me.
- It's very slow. Almost all parts of lodash/fp are orders of magnitute slower than the idiomatic, hand-written alternative.
Why not ramda?
- The error handling is very inconsistent. Some functions return garbage output given garbage input, some return nothing, and some throw errors. I don't want to remember which functions do what when writing my code.
- It's very slow. Almost all parts of ramda are orders of magnitute slower than the idiomatic, hand-written alternative.
- Ramda has a function for everything, so the temptation to “ramdafy” everything is enormous. I personally find that giving in to this temptation leads to completely incomprehensible code due to the highly abstract nature of most of the utility functions. It quickly becomes a language in its own right.
API Reference
All of the functions exported by this library are of the shape:
(options) -> (...arguments) -> value?
That is, they are all called like this: const fn = succor[function](options); // WILL throw on bad options.
const value = fn(...arguments); // Will NEVER throw on bad arguments.
get
Safely “gets” a property at an arbitrary path inside of nested objects. Returns “undefined” if the path doesn't lead to a value.
get: (path) => (arg) => ret
where path: Array<Number | String | Symbol>
arg : Any
ret : Any
- path
Must be an Array whose elements are each either Number, String, or Symbol. Throws an error otherwise as these are the only valid indexes for paths.
- arg
Anything, but should be an object, array, or other indexable entity to return a value.
- ret
-
Returns the result of safely indexing into the first index in path, then the second index in path, etc. until an index doesn't exist in the argument or all indexes in the path have been looked up.
Although it's not the actual implementation you can imagine it working like:
arg?.[path[0]]?.[path[1]]?.…?.path[n]
Examples:
const s = Symbol('some symbol');
get(['a', 0, s])({ a: [{ [s]: 5 }] });
// => 5
get(['a', 0, s])(undefined)
// => undefined
get({})
// throws Error
pick
Safely “picks” the given properties at arbitrary paths inside of a nested object, returning a new object with only those properties.
pick: (paths) => (arg) => ret
where paths: Array<Array<Number | String | Symbol>>
val : Any
arg : Any
ret : Any
- paths
Think of it as an Array of the kind of path you'd give to “get”. Must be an Array of: Arrays whose elements are each either Number, String, or Symbol. Throws an error otherwise as these are the only valid indexes for paths.
- arg
Anything, but should be an object, array, or other indexable entity to return a value.
- ret
-
Returns a new object that picks out the properties at the given paths. For each path it will safely index into the first index in path, then the second index in path, etc. until an index doesn't exist in the argument or all indexes in the path have been looked up. It will then set this value, if it exists, in the resulting object.
Although it's not the actual implementation you can imagine it working like
arg?.[path[0]]?.[path[1]]?.…?.path[n]
for each path in paths.
Examples:
const s = Symbol('some symbol');
pick([['a', 0, s], ['b']])({ a: [{ [s]: 5 }, 5], b: 5, c: 5 });
=> { a: [{ [s]: 5 }], b: 5 }
pick([['a', 0, s], ['b']])(undefined)
=> {}
pick({})
=> Throws Error
set
Safely “sets” a property at an arbitrary path inside of a nested object. Will create any missing objects alongs the path to ensure an object is always returned with the given value at the given path. Never mutates its arguments.
set: (path, val) => (arg) => ret
where path: Array<Number | String | Symbol>
val : Any
arg : Any
ret : Any
- path
Must be an Array whose elements are each either Number, String, or Symbol. Throws an error otherwise as these are the only valid indexes for paths.
- val
Anything.
- arg
Anything, but should be an object, array, or other indexable entity to make much sense.
- ret
-
Returns a new object that clones the necessary parts of arg to ensure that val is found at path, but without ever mutating arg. If it is not possible to set the path in arg (arg might be undefined, a number, etc.) a completely new object will be returned to keep the contract.
Examples:
const s = Symbol('some symbol');
set(['a', 0, s], 'value')({ a: [{ [s]: 5 }], b: 5 });
=> { a: [{ [s]: 'value' }], b: 5 }
set(['a', 0, s], 'value')(undefined)
=> { a: [{ [s]: 'value' }] }
set({})
=> Throws Error