omnitool
v0.0.8
Published
Utility library centered on strong typing, simple FP, and unified iterable operations [EARLY]
Downloads
4
Readme
Omnitool
Omnitool is a strongly typed, general purpose utility library for JavaScript and TypeScript.
Alternatives
- LoDash / Underscore
- Ramda
Features
- Fully typed from the start
- Type information is never lost
- Iterable operations work on all iterables
- Iterable operations are chainable
- Immutability is favored over mutability
- Mutation is rare and obvious
- Errors are favored over null/undefined
* The following documentation is a work in progress. *
Getting Started
import { O } from "omnitool"
/* Or: const O = require("omnitool") */
const result = O.range(10).done()
console.log(result)
Iterable Operations
For the most part, the iteration functions of Omnitool are its defining factor.
Omnitool's iteration functions are mostly based on underlying iterable objects called omni-sequences (OmniSequence). These objects wrap arrays, strings, generators and all other iterables and add extended functionality. Omni-sequences come in two variants, pipes (Pipe) and spans (Span). Generally you won't have to worry about which one you are using as you will interact with both through the OmniSequence interface.
The flow of manipulating an iterable with Omnitool is usually one of the following:
- Iterable -> Omni-sequence(s) -> Iterable
- Iterable -> Omni-sequence(s) -> Value
- Iterable -> Value
So the omni-sequence is the middle man that makes things more general. Think of containers (arrays, objects, maps, sets...) as solids and omni-sequences as a fluid you can shape into whatever you want.
Notes
- Omni-sequences should generally only be used when transforming or iterating data, not storing it
- They are only guaranteed to be iterable once
- Operations are immutable
- Operations can be called as free functions (O.map($iterable, $function)) or via property access ($omni-sequence.map($function)).
Pipes
Pipes are omni-sequences in which each element is created lazily, meaning they are created as they are used. Pipes are really just generators with extended functionality.
Pipes are usually returned when an operation delivers a sequence of elements that may have been:
- Transformed in some way (O.map())
- Non-continguous in the source iterable (O.filter())
- Generated without a source (O.range())
- Generated infinitely (O.sequence())
- Sourced from something other than an array or string
for (const i of O.range(100)) {
console.log(i)
}
In the above code, O.range() does not create an array, but a pipe that generates the integers from 0 to 99. If we do want an array we can simply call the done() function on the pipe:
O.range(100).done() /* ==> [0, 1, 2, 3, ..., 99] */
Because pipes don't have to hold the memory of all elements at once, they can yield elements infinitely.
const increment = (number) => number + 1
O.sequence(0, increment) /* ==> (0, 1, 2, 3, 4, 5, ...) */
O.sequence(0, increment).take(3).done() /* ==> [0, 1, 2] */
Pipe Examples
const numbers = [1, 2, 3, 4, 5]
const object = {
a: 1,
b: 2,
c: 3
}
const even = (number) => number % 2 === 0
const increment = (number) => number + 1
const double = (number) => number * 2
const toString = (element) => element.toString()
O.filter(numbers, even) /* ==> (0, 2, 4) */
O.map(numbers, toString) /* ==> ("1", "2", "3", "4", "5") */
O.map(numbers, toString).repeat() /* ==> ("1", "2", "3", "4", "5", "1", "2", ...) */
O.repeat(numbers) /* ==> (1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...) */
O.repeat(numbers).take(6) /* ==> (1, 2, 3, 4, 5, 1) */
O.sequence(() => 0) /* ==> (0, 0, 0, 0, ...) */
O.sequence(() => 0).take(3).done() /* ==> [0, 0, 0] */
O.chunk(numbers, 2) /* ==> ([1, 2], [3, 4], [5]) */
O.permute([1, 2, 3]) /* ==> ([ 0, 1, 2 ],
[ 1, 0, 2 ],
[ 2, 0, 1 ],
[ 0, 2, 1 ],
[ 1, 2, 0 ],
[ 2, 1, 0 ]) */
O.sequence(0, increment).filter(even) /* ==> (0, 2, 4, 6, ...) */
O.sequence(0, increment).filter(even).map(toString) /* ==> ("0", "2", "4", "6", ...) */
O.sequence(1, double) /* ==> (1, 2, 4, 8, ...) */
O.sequence(1, double).take(3).done() /* ==> [1, 2, 4] */
O.pairs(object) /* ==> (["a", 1], ["b", 2], ["c", 3]) */
O.pairs(object).map(([key, value]) => { /* ==> (["A", 2], ["B", 3], ["C", 4]) */
return [O.upper(key), value + 1]
})
O.pairs(object).map(([key, value]) => { /* ==> {"A": 2, "B": 3, "C": 4} */
return [O.upper(key), value + 1]
}).toObject()
Spans
Spans are omni-sequences that represent a slice of an existing array or string. They are usually more performant than pipes. Spans support O(1) access time rather than O(n) and can be iterated multiple times.
Spans are usually returned when an operation delivers a group of elements that may have been:
- Drawn from a contiguous section of an array, string or other span (O.slice())
- Placed into a new array that needed to be allocated anyway (O.takeLast() on a pipe)
const numbers = [1, 2, 3, 4, 5]
O.slice(numbers, 1, 3) /* ==> (2, 3) */
In the above code, O.slice() does not allocate a new array, but a span over the source array "numbers" from a start index (1) to a non-inclusive end index (3). Because of this, slice() and slice-like operations are relatively cheap. The slice() function in particular becomes an O(1) operation when used on arrays, strings and other spans.
Once again, if we do want an array we can just call the done() function.
O.slice(numbers, 1, 3).done() /* ==> [2, 3] */
Pipe Examples
const numbers = [1, 2, 3, 4, 5]
const lessThanThree = (number) => number < 3
const greaterThanThree = (number) => number > 3
O.slice(numbers, 1, 3) /* ==> (2, 3) */
O.drop(numbers, 2) /* ==> (3, 4, 5) */
O.drop(numbers, lessThanThree) /* ==> (3, 4, 5) */
O.take(numbers, 2) /* ==> (1, 2) */
O.take(numbers, lessThanThree) /* ==> (1, 2) */
O.dropLast(numbers) /* ==> (1, 2, 3, 4) */
O.dropLast(numbers, 2) /* ==> (1, 2, 3) */
O.takeLast(numbers, 2) /* ==> (4, 5) */
O.dropLast(numbers, greaterThanThree) /* ==> (1, 2, 3) */
O.takeLast(numbers, greaterThanThree) /* ==> (4, 5) */
On Infinite Sequences
With Omnitool, it's simple to create and use infinite sequences. The following expression generates an infinite sequence of all powers of two:
const double = (number) => number * 2
O.sequence(1, double) /* ==> (1, 2, 4, 8, 16, ...) */
To get a finite sequence or some another value out of an infinite sequence, use a function that limits the bounds.
O.sequence(1, double).take(5).done() /* ==> [1, 2, 4, 8, 16] */
O.sequence(1, double).take((current) => x <= 16).done() /* ==> [1, 2, 4, 8, 16] */
O.sequence(1, double).at(3) /* ==> 8 */
Be careful with infinite sequences. It's rather easy to get into infinite loops.
const increment = (number) => number + 1
O.sequence(0, increment).done() /* PROBLEM: Infinitely large array */
O.sequence(0, increment).last() /* PROBLEM: No last element */
O.sequence(0, increment).lastIndex() /* PROBLEM: No last index */
O.sequence(0, increment).takeLast(3) /* PROBLEM: No last elements to take */