rubico
🏞 a shallow river in northeastern Italy, just south of Ravenna
Motivation
You are suddenly dropped into a world where all people write code in assembly.
There is no "High Level Language", only "Assembly Language".
There is no C, just ASM. There are no variables, only registers.
You are in charge of managing all memory in your programs: mov
ing data from register to register, push
ing and pop
ing data on the hardware supported stack.
How would you write a webserver, or a database? How long would that take? How much longer would it take you to do whatever it is that you are currently doing?
We need not stay here any longer.
...interdimensional warp...
Welcome back to reality, where the world is rife with programming languages above assembly
How did this come to be? Why would anyone not want to spend their day to day in assembly? According to an answer thread on stack overflow,
ASM has poor legibility and isn't really maintainable compared to higher-level languages.
[Assembly] takes more code to do the same thing as in a high-level languge, and there is a direct correlation between lines of code and bugs.
Another take from wikipedia
In contrast to low-level programming languages, [high-level programming languages] may use natural language elements, be easier to use, or may automate (or even hide entirely) significant areas of computing systems (e.g. memory management), making the process of developing a program simpler and more understandable than when using a lower-level language.
Perhaps the abundance of higher level languages comes down to readability versus performance
First code for correctness, then for clarity (the two are often connected, of course!). Finally, and only if you have real empirical evidence that you actually need to, you can look at optimizing.
IMO the obvious readable version first, until performance is measured and a faster version is required.
I would go for readability first.
Great, looks like people are for readability, so where does that leave us? And what does any of this have to do with rubico?
Consider these two samples of JavaScript code. Both execute an asynchronous function for every element of an array
Promise.all(array.map(doAsyncThing)) // vanilla JavaScript
map(doAsyncThing)(array) // rubico
It looks like you can write a little less to do the same thing with rubico. Is the rubico version more readable? I'd say it's up for debate.
What if we wanted to do multiple asynchronous things in parallel for every item of the array?
Promise.all([
Promise.all(array.map(doAsyncThingA)),
Promise.all(array.map(doAsyncThingB)),
Promise.all(array.map(doAsyncThingC)),
]) // vanilla JavaScript
map(fork([
doAsyncThingA,
doAsyncThingB,
doAsyncThingC,
]))(array) // rubico
It looks like vanilla JavaScript has four more Promise.all
statements, and two more map
keywords.
rubico, on the other hand, has one map
and one fork
. Simpler? Hold your horses.
What if we now want to do another async thing per item of each of the responses?
Promise.all([
Promise.all(array.map(doAsyncThingA).then(
arrayA => Promise.all(arrayA.map(doAsyncThingAA))
)),
Promise.all(array.map(doAsyncThingB).then(
arrayB => Promise.all(arrayB.map(doAsyncThingBB))
)),
Promise.all(array.map(doAsyncThingC).then(
arrayC => Promise.all(arrayC.map(doAsyncThingCC))
)),
]) // vanilla JavaScript
map(fork([
pipe([doAsyncThingA, map(doAsyncThingAA)]),
pipe([doAsyncThingB, map(doAsyncThingBB)]),
pipe([doAsyncThingC, map(doAsyncThingCC)]),
]))(array) // rubico
I think it's safe to say that rubico is more expressive here.
Back to assembly. You could compare what rubico does for JavaScript to what C does for assembly.
In contrast to assembly, C uses natural language elements, is easier to use, and automates (but doesn't hide entirely) memory management. C makes the process of developing a program simpler and more understandable than when using assembly.
In contrast to vanilla JavaScript, rubico automates (hides entirely) the cruft surrounding Promises. rubico makes the process of developing a program simpler and more understandable than when using vanilla JavaScript.
I should also mention that when you use rubico, you get the benefits of the functional programming paradigm (but not the confusion) for free.
At your leisure, motivations 1, 2, and 3 for functional programming.
Introduction
rubico resolves the Promise.all
of three Promises
- simplify asynchronous programming
- enable functional programming
- free transducers
programs written with rubico follow a functional style, otherwise known as data last.
This is data first
[1, 2, 3, 4, 5].map(number => number * 2) // => [2, 4, 6, 8, 10]
This is data last
map(number => number * 2)([1, 2, 3, 4, 5]) // => [2, 4, 6, 8, 10]
Data last saves you brain power when you name things
const xyz = async x => {
const resultOfFoo = await foo(x)
const resultOfBar = await bar(resultOfFoo)
const resultOfBaz = await baz(resultOfBar)
return resultOfBaz
} // data first
const xyz = pipe([foo, bar, baz]) // data last
Installation
with npm
npm i rubico
with deno
import {
pipe, fork, assign, tap, tryCatch, switchCase,
map, filter, reduce, transform,
any, all, and, or, not,
eq, gt, lt, gte, lte,
get, pick, omit,
} from 'https://deno.land/x/rubico/rubico.js'
browser script, global rubico
<script src="https://unpkg.com/rubico/index.js" crossorigin></script>
browser module
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Title Here</title>
<script type="module">
import {
pipe, fork, assign, tap, tryCatch, switchCase,
map, filter, reduce, transform,
any, all, and, or, not,
eq, gt, lt, gte, lte,
get, pick, omit,
} from 'https://deno.land/x/rubico/rubico.js'
// your code here
</script>
</head>
<body></body>
</html>
System Requirements
- minimum node version: 10.3
Documentation
rubico aims to hit the sweet spot between expressivity and interface surface area.
There are 23 functions at the moment; this number is not expected to go up much more or at all.
Instead, some methods will have property functions that represent the same signature (i.e. map
vs map.series
)
but exhibit differences in behavior (i.e. map
executes in parallel while map.series
executes in series).
[series] and [parallel] are tags to denote the asynchronous behavior of methods that accept multiple functions.
- [series]: execute functions one at a time. If order is not implied, it is left to the implementation. (i.e. iterating an
Object
) - [parallel]: execute functions in parallel.
All higher order functions accept sync or async functions; if all provided functions are synchronous, the entire execution is synchronous.
function composition
- pipe [series] - chain functions together
- tap - spy on data
- tryCatch [series] - try a function, catch with another
- switchCase [series] - control flow
function + data composition
- fork [parallel] - multiply data by functions
fork.series
[series]
- assign [parallel] - set properties on data by functions
data transformation
- map [parallel] - apply a function to data
map.pool
[parallel] -map
with asynchronous limitmap.withIndex
[parallel] -map
with indexmap.series
[series]map.sleep
[series] -map.series
with delay in between todo
- filter [parallel] - exclude data by predicate
filter.withIndex
[parallel] -filter
with index
- reduce [series] - execute data transformation (powerful)
- transform [series] - execute data transformation (convenient)
predicate composition
- any [parallel] - is function of any data truthy?
- all [parallel] - is function of all data truthy?
- and [parallel] - any functions of data truthy?
- or [parallel] - all functions of data truthy?
- not -
not(equals)(x)
is!equals(x)
comparison
- eq [parallel] - left equals right?
- gt [parallel] - left > right?
- lt [parallel] - left < right?
- gte [parallel] - left >= right?
- lte [parallel] - left <= right?
property + index access
- get - access a value by path or index
data composition
- pick - only allow provided properties
pick.range
- only allow provided range todo
- omit - exclude provided properties
pipe
chains functions from left to right in series
y = pipe(functions)(x)
functions
is an array of functions
x
is anything
if x
is a function, pipe chains functions
from right to left,
see transducers
y
is the output of running x
through the chain of functions
y
is wrapped in a Promise if any of the following are true:
- any function of
functions
is asynchronous
pipe([
x => x + ' ',
x => x + 'world',
])('hello') // => 'hello world'
pipe([
async x => x + ' ',
x => x + 'world',
])('hello') // => Promise { 'hello world' }
fork
parallelizes functions with data, retaining functions' type and shape
y = fork(functions)(x)
functions
is an array of functions or an object of functions
all functions of functions
are run concurrently
x
is anything
if functions
is an array, y
is functions.map(f => f(x))
if functions
is an object, y
is an object of entries key: f(x)
for entry key: f
of functions
y
is wrapped in a Promise if any of the following are true:
- any function of
functions
is asynchronous
fork([
x => x + 'world',
x => x + 'mom'
])('hello') // => ['hello world', 'hello mom']
fork([
x => x + 'world',
async x => x + 'mom'
])('hello') // => Promise { ['hello world', 'hello mom'] }
fork({
a: x => x + 'world',
b: x => x + 'mom',
})('hello') // => { a: 'hello world', b: 'hello mom' }
fork({
a: x => x + 'world',
b: async x => x + 'mom',
})('hello') // => Promise { { a: 'hello world', b: 'hello mom' } }
fork.series
executes functions with data in series, retaining functions' type and shape
y = fork.series(functions)(x)
assign
parallelizes functions with data, merging output into data
y = assign(functions)(x)
functions
is an object of functions
all functions of functions
are run concurrently
x
is an object
output
is an object of entries key: f(x)
for entry key: f
of functions
y
is output
merged into x
y
is wrapped in a Promise if any of the following are true:
- any function of
functions
is asynchronous
assign({
hi: x => 'hi ' + x,
bye: x => 'bye ' + x,
})({ name: 'Ed' }) // => { name: 'Ed', hi: 'hi Ed', bye: 'bye Ed' }
assign({
async hi: x => 'hi ' + x,
bye: x => 'bye ' + x,
})({ name: 'Ed' }) // => Promise { { name: 'Ed', hi: 'hi Ed', bye: 'bye Ed' } }
assign({
name: () => 'not Ed',
})({ name: 'Ed' }) // => { name: 'not Ed' }
tap
calls a function with data, returning data
y = tap(f)(x)
x
is anything
f
is a function that expects one argument x
y
is x
y
is wrapped in a Promise if any of the following are true:
f
is asynchronous
if x
is a function, y
is a transduced reducing function, see transducers
y = tap(f)(x); reduced = reduce(y)(z)
reduce
is reduce,
z
is an iterable, async iterable, or object
zi
is an element of z
f
is a function that expects one argument zi
reduced
is equivalent to reduce(x)(z)
tap(
console.log, // > 'hey'
)('hey') // => 'hey'
const asyncConsoleLog = async x => console.log(x)
tap(
asyncConsoleLog, // > 'hey'
)('hey') // => Promise { 'hey' }
const concat = (y, xi) => y.concat([xi])
reduce(
tap(console.log)(concat), // > 1 2 3 4 5
[],
)([1, 2, 3, 4, 5]) // => [1, 2, 3, 4, 5]
tryCatch
tries a function with data, catches with another function
y = tryCatch(f, g)(x)
f
is a function that expects one argument x
g
is a function that expects two arguments err
and x
x
is anything
err
is a value potentially thrown by f(x)
if f(x)
throws err
, y
is g(err, x)
, else y
is f(x)
y
is wrapped in a Promise if any of the following are true:
f
is asynchronousf
is synchronous,g
is asynchronous, andf(x)
threw
const onError = (e, x) => `${x} is invalid: ${e.message}`
tryCatch(
x => x,
onError,
)('hello') // => 'hello'
const throwGoodbye = () => { throw new Error('goodbye') }
tryCatch(
throwGoodbye,
onError,
)('hello') // => 'hello is invalid: goodbye'
const rejectWithGoodbye = () => Promise.reject(new Error('goodbye'))
tryCatch(
rejectWithGoodbye,
onError,
)('hello') // => Promise { 'hello is invalid: goodbye' }
switchCase
an if, else if, else construct for functions
y = switchCase(functions)(x)
x
is anything
functions
is an array of functions
given
- predicate
if
functionsif1, if2, ..., ifN
- corresponding
do
functionsdo1, do2, ..., doN
- an
else
functionelseDo
functions
is an array of functions
[
if1, do1,
if2, do2,
..., ...,
elseDo,
]
switchCase evaluates functions in functions
from left to right
y
is the first do(x)
whose corresponding if(x)
is truthy
y
is wrapped in a Promise if any of the following are true:
- any evaluated functions are asynchronous
const isOdd = x => x % 2 === 1
switchCase([
isOdd, () => 'odd',
() => 'even',
])(1) // => 'odd'
switchCase([
async isOdd, () => 'odd',
() => 'even',
])(1) // => Promise { 'odd' }
map
applies a function to each element of data in parallel, retaining data type and shape
y = map(f)(x)
x
is an iterable, an async iterable, an object, or a function
xi
is an element of x
f
is a function that expects one argument xi
y
is of type and shape x
with f
applied to each element, with some exceptions:
- if
x
is an async iterable but not a built-in type,y
is a generated async iterable - if
x
is an iterable but not a built-in type,y
is a generated iterable - if
x
is an iterable but not a built-in type andf
is asynchronous,y
is an iterable of promises
y
is wrapped in a Promise if any of the following are true:
f
is asynchronous andx
is not an async iterable
if x
is a function, y
is a transduced reducing function, see transducers
y = map(f)(x); reduced = reduce(y)(z)
reduce
is reduce,
z
is an iterable, async iterable, or object
zi
is an element of z
f
is a function that expects one argument zi
x
is a reducing function that expects two arguments y
and f(zi)
reduced
is equivalent to reduce(x)(map(f)(z))
const square = x => x ** 2
map(
square,
)([1, 2, 3, 4, 5]) // => [1, 4, 9, 16, 25]
const asyncSquare = async x => x ** 2
map(
asyncSquare,
)([1, 2, 3, 4, 5]) // => Promise { [1, 4, 9, 16, 25] }
map(
Math.abs,
)(new Set([-2, -1, 0, 1, 2])) // => { Set { 0, 1, 2 } }
const double = ([k, v]) => [k + k, v + v]
map(
double,
)(new Map([['a', 1], ['b', 2]])) // => Map { 'aa' => 2, 'bb' => 4 }
map(
byte => byte + 1,
)(new Uint8Array([97, 98, 99])) // Uint8Array [ 98, 99, 100 ]
map(
word => word + 'z',
)({ a: 'lol', b: 'cat' }) // => { a: 'lolz', b: 'catz' }
map.pool
Apply a function to every element of data in parallel with limited concurrency
y = map.pool(size, f)(x)
map.withIndex
Apply a function to every element of data in parallel with index and reference to data
y = map.withIndex(f)(x); yi = f(xi, i, x)
map.series
Apply a function to every element of data in series
y = map.series(f)(x)
map.seriesWithIndex
Apply a function to every element of data in series with index and reference to data
= map.seriesWithIndex(f)(x); yi = f(xi, i, x)
filter
filters elements out of data in parallel based on provided predicate
y = filter(f)(x)
x
is an iterable, an async iterable, an object, or a function
xi
is an element of x
f
is a function that expects one argument xi
y
is of type and shape x
with elements xi
where f(xi)
is truthy, with some exceptions:
- if
x
is an async iterable but not a built-in type,y
is a generated async iterable - if
x
is an iterable but not a built-in type,y
is a generated iterable - if
x
is an iterable but not a bulit-in type andf
is asynchronous, filter will throw a TypeError
y
is wrapped in a Promise if any of the following are true:
f
is asynchronous andx
is not an async iterable
if x
is a function, y
is a transduced reducing function, see transducers
y = filter(f)(x); reduced = reduce(y)(z)
reduce
is reduce,
z
is an iterable, async iterable, or object
zi
is an element of z
f
is a function that expects one argument zi
x
is a reducing function that expects two arguments y
and zi
reduced
is equivalent to reduce(x)(filter(f)(z))
const isOdd = x => x % 2 === 1
filter(
isOdd,
)([1, 2, 3, 4, 5]) // => [1, 3, 5]
const asyncIsOdd = async x => x % 2 === 1
filter(
asyncIsOdd,
)([1, 2, 3, 4, 5]) // => Promise { [1, 3, 5] }
filter(
letter => letter !== 'y',
)('yoyoyo') // => 'ooo'
const abcSet = new Set(['a', 'b', 'c'])
filter(
letter => abcSet.has(letter),
)(new Set(['a', 'z'])) // => Set { 'a' }
filter(
([key, value]) => key === value,
)(new Map([[0, 1], [1, 1], [2, 1]])) // => { Map { 1 => 1 } }
filter(
bigint => bigint <= 3n,
)(new BigInt64Array([1n, 2n, 3n, 4n, 5n])) // => BigInt64Array [1n, 2n, 3n]
filter(
value => value === 1,
)({ a: 1, b: 2, c: 3 }) // => { a: 1 }
filter.withIndex
Filter, but with each predicate called with index and reference to data
y = filter.withIndex(f)(x); yi = f(xi, i, x)
reduce
transforms data in series according to provided reducing function and initial value
y = reduce(f, x0)(x)
x
is an iterable, an async iterable, or an object
xi
is an element of x
f
is a reducing function that expects two arguments y
and xi
x0
is optional, and if provided:
y
starts asx0
- iteration begins with the first element of
x
if x0
is not provided:
y
starts as the first element ofx
- iteration begins with the second element of
x
y
is f(y, xi)
for each successive xi
y
is wrapped in a Promise if any of the following are true:
f
is asynchronousx
is an async iterable
const add = (y, xi) => y + xi
reduce(
add,
)([1, 2, 3, 4, 5]) // => 15
reduce(
add, 100,
)([1, 2, 3, 4, 5]) // => 115
const asyncAdd = async (y, xi) => y + xi
reduce(
asyncAdd,
)([1, 2, 3, 4, 5]) // => Promise { 15 }
const asyncNumbersGeneratedIterable = (async function*() {
for (let i = 0; i < 5; i++) { yield i + 1 }
})() // generated async iterable that yields 1 2 3 4 5
const concat = (y, xi) => y.concat([xi])
reduce(
concat, [],
)(asyncNumbersGeneratedIterable) // => Promise { [1, 2, 3, 4, 5] }
reduce(
concat, [],
)({ a: 1, b: 1, c: 1, d: 1, e: 1 }) // => [1, 2, 3, 4, 5]
transform
transforms data in series according to provided transducer and initial value
y = transform(f, x0)(x)
x
is an iterable, an async iterable, or an object
f
is a transducer, see transducers
x0
is null, an array, a string, a set, a map, a typed array, or a writable
y
is x
transformed with f
into x0
y
is wrapped in a Promise if any of the following are true:
f
is asynchronousx
is an async iterable
in the following examples, map
is map
const square = x => x ** 2
transform(map(
square,
), null)([1, 2, 3, 4, 5]) // => null
const asyncSquare = async x => x ** 2
transform(map(
asyncSquare,
), [])([1, 2, 3, 4, 5]) // => Promise { [1, 4, 9, 16, 25] }
transform(map(
square,
), '')([1, 2, 3, 4, 5]) // => '1491625'
transform(map(
square,
), new Set())([1, 2, 3, 4, 5]) // => Set { 1, 4, 9, 16, 25 }
transform(map(
number => [number, square(number)],
), new Map())([1, 2, 3, 4, 5]) // => Map { 1 => 1, 2 => 4, 3 => 9, 4 => 16, 5 => 25 }
const charToByte = x => x.charCodeAt(0)
transform(map(
square,
), new Uint8Array())([1, 2, 3, 4, 5]), // => Uint8Array [1, 4, 9, 16, 25]
const asyncNumbersGeneratedIterable = (async function*() {
for (let i = 0; i < 5; i++) { yield i + 1 }
})() // generated async iterable that yields 1 2 3 4 5
transform(map(
square,
), process.stdout)(asyncNumbersGeneratedIterable) // > 1 4 9 16 25
// => Promise { process.stdout }
any
applies a function to each element of data parallel, returns true if any evaluations truthy
y = any(f)(x)
x
is an iterable or an object
xi
is an element of x
f
is a function that expects one argument xi
y
is true if all f(xi)
are truthy, false otherwise
y
is wrapped in a Promise if any of the following are true:
f
is asynchronous
const isOdd = x => x % 2 === 1
any(
isOdd,
)([1, 2, 3, 4, 5]) // => true
const asyncIsOdd = async x => x % 2 === 1
any(
asyncIsOdd,
)([1, 2, 3, 4, 5]) // => Promise { true }
any(
isOdd,
)({ b: 2, d: 4 }) // => false
all
applies a function to each element of data in parallel, returns true if all evaluations truthy
y = all(f)(x)
x
is an iterable or an object
xi
is an element of x
f
is a function that expects one argument xi
y
is true if any f(xi)
are truthy, false otherwise
y
is wrapped in a Promise if any of the following are true:
f
is asynchronous
const isOdd = x => x % 2 === 1
all(
isOdd,
)([1, 2, 3, 4, 5]) // => false
const asyncIsOdd = async x => x % 2 === 1
all(
asyncIsOdd,
)([1, 2, 3, 4, 5]) // => Promise { false }
all(
isOdd,
)({ a: 1, c: 3 }) // => true
and
applies each function of functions in parallel to data, returns true if all evaluations truthy
y = and(functions)(x)
x
is anything
functions
is an array of functions
f
is a function of functions
y
is true if all f(x)
are truthy, false otherwise
y
is wrapped in a Promise if any of the following are true:
- any
f
is asynchronous
const isOdd = x => x % 2 === 1
const asyncIsOdd = async x => x % 2 === 1
const lessThan3 = x => x < 3
and([
isOdd,
lessThan3,
])(1) // => true
and([
asyncIsOdd,
lessThan3,
])(1) // => Promise { true }
and([
isOdd,
lessThan3,
])(2) // => false
or
applies each function of functions in parallel to data, returns true if any evaluations truthy
y = or(functions)(x)
x
is anything
functions
is an array of functions
f
is a function of functions
y
is true if any f(x)
are truthy, false otherwise
y
is wrapped in a Promise if any of the following are true:
- any
f
is asynchronous
const isOdd = x => x % 2 === 1
const asyncIsOdd = async x => x % 2 === 1
const lessThan3 = x => x < 3
or([
isOdd,
lessThan3,
])(5) // => true
or([
asyncIsOdd,
lessThan3,
])(5) // => Promise { true }
or([
isOdd,
lessThan3,
])(6) // => false
not
applies a function to data, logically inverting the result
y = not(f)(x)
x
is anything
f
is a function that expects one argument x
y
is true if f(x)
is falsy, false otherwise
y
is wrapped in a Promise if any of the following are true:
f
is asynchronous
const isOdd = x => x % 2 === 1
const asyncIsOdd = async x => x % 2 === 1
not(
isOdd,
)(2) // => true
not(
asyncIsOdd,
)(2) // => Promise { true }
not(
isOdd,
)(3) // => false
eq
tests left strictly equals right
y = eq(left, right)(x)
x
is anything
left
is a non-function value or a function that expects one argument x
right
is a non-function value or a function that expects one argument x
leftCompare
is left
if left
is a non-function value, else left(x)
rightCompare
is right
if right
is a non-function value, else right(x)
y
is true if leftCompare
strictly equals rightCompare
, false otherwise
y
is wrapped in a Promise if any of the following are true:
left
is asynchronousright
is asynchronous
const square = x => x ** 2
const asyncSquare = async x => x ** 2
eq(
square,
1,
)(1) // => true
eq(
asyncSquare,
1,
)(1) // => Promise { true }
eq(
square,
asyncSquare,
)(1) // => Promise { true }
eq(
1,
square,
)(2) // => false
eq(1, 1)() // => true
eq(0, 1)() // => false
gt
tests left greater than right
y = gt(left, right)(x)
x
is anything
left
is a non-function value or a function that expects one argument x
right
is a non-function value or a function that expects one argument x
leftCompare
is left
if left
is a non-function value, else left(x)
rightCompare
is right
if right
is a non-function value, else right(x)
y
is true if leftCompare
is greater than rightCompare
y
is wrapped in a Promise if any of the following are true:
left
is asynchronousright
is asynchronous
gt(
x => x,
10,
)(11) // => true
gt(
async x => x,
10,
)(11) // => Promise { true }
gt(
x => x,
10,
)(9) // => false
gt(2, 1)() // => true
gt(1, 1)() // => false
gt(0, 1)() // => false
lt
tests left less than right
y = lt(left, right)(x)
x
is anything
left
is a non-function value or a function that expects one argument x
right
is a non-function value or a function that expects one argument x
leftCompare
is left
if left
is a non-function value, else left(x)
rightCompare
is right
if right
is a non-function value, else right(x)
y
is true if leftCompare
is less than rightCompare
y
is wrapped in a Promise if any of the following are true:
left
is asynchronousright
is asynchronous
lt(
x => x,
10,
)(9) // => true
lt(
async x => x,
10,
)(9) // => Promise { true }
lt(
x => x,
10,
)(11) // => false
lt(0, 1)() // => true
lt(1, 1)() // => false
lt(2, 1)() // => false
gte
tests left greater than or equal right
y = gte(left, right)(x)
x
is anything
left
is a non-function value or a function that expects one argument x
right
is a non-function value or a function that expects one argument x
leftCompare
is left
if left
is a non-function value, else left(x)
rightCompare
is right
if right
is a non-function value, else right(x)
y
is true if leftCompare
is greater than or equal to rightCompare
y
is wrapped in a Promise if any of the following are true:
left
is asynchronousright
is asynchronous
gte(
x => x,
10,
)(11) // => true
gte(
async x => x,
10,
)(11) // => Promise { true }
gte(
x => x,
10,
)(9) // => false
gte(2, 1)() // => true
gte(1, 1)() // => true
gte(0, 1)() // => false
lte
tests left less than or equal right
y = lte(left, right)(x)
x
is anything
left
is a non-function value or a function that expects one argument x
right
is a non-function value or a function that expects one argument x
leftCompare
is left
if left
is a non-function value, else left(x)
rightCompare
is right
if right
is a non-function value, else right(x)
y
is true if leftCompare
is less than or equal to rightCompare
y
is wrapped in a Promise if any of the following are true:
left
is asynchronousright
is asynchronous
lte(
x => x,
10,
)(9) // => true
lte(
async x => x,
10,
)(9) // => Promise { true }
lte(
x => x,
10,
)(11) // => false
lte(0, 1)() // => true
lte(1, 1)() // => true
lte(2, 1)() // => false
get
accesses a property by path
y = get(path, defaultValue)(x)
x
is an object
path
is a number, string, a dot-delimited string, or an array
defaultValue
is optional; if not provided, it is undefined
y
depends on path
:
- if
path
is a number or string,y
isx[path]
- if
path
is a dot-delimited string'p.a...t.h'
,y
isx['p']['a']...['t']['h']
- if
path
is an array['p', 'a', ..., 't', 'h']
,y
isx['p']['a']...['t']['h']
- if
path
is not found inx
,y
isdefaultValue
get('a')({ a: 1, b: 2 }) // => 1
get('a')({}) // => undefined
get('a', 10)({}) // => 10
get(0)(['hello', 'world']) // => 'hello'
get('a.b.c')({ a: { b: { c: 'hey' } } }) // => 'hey'
get([0, 'user', 'id'])([
{ user: { id: '1' } },
{ user: { id: '2' } },
]) // => '1'
pick
constructs a new object from data composed of the provided properties
y = pick(properties)(x)
x
is an object
properties
is an array of strings
y
is an object composed of all properties enumerated in properties
and defined in x
pick(['a', 'b'])({ a: 1, b: 2, c: 3 }) // => { a: 1, b: 2 }
pick(['d'])({ a: 1, b: 2, c: 3 }) // => {}
omit
constructs a new object from data without the provided properties
y = omit(properties)(x)
x
is an object
properties
is an array of strings
y
is an object composed of every property in x
except for those enumerated in properties
omit(['a', 'b'])({ a: 1, b: 2, c: 3 }) // => { c: 3 }
omit(['d'])({ a: 1, b: 2, c: 3 }) // => { a: 1, b: 2, c: 3 }
Transducers
Transducers enable us to wrangle very large or infinite streams of data in a
composable and memory efficient way. Say you had veryBigData
in an array
veryBigData = [...]
veryBigFilteredData = veryBigData.filter(datum => datum.isBig === true)
veryBigProcessedData = veryBigFilteredData.map(memoryIntensiveProcess)
console.log(veryBigProcessedData)
The above is not very memory efficient because of the intermediate arrays veryBigFilteredData
and veryBigProcessedData
. We're also logging out a large quantity of data at once to the console.
With rubico, you could express the above transformation as a single pass
without incurring a memory penalty
veryBigData = [...]
transform(pipe([
filter(datum => datum.isBig === true),
map(memoryIntensiveProcess),
]), process.stdout)(veryBigData)
In this case, pipe([filter(...), map(...)])
is a transducer, and we're writing each datum
to the console via process.stdout
. transform
consumes our pipe([filter(...), map(...)])
transducer and supplies it with veryBigData
.
Behind the scenes, transform
is calling reduce
with a reducing function suitable for writing
to process.stdout
converted from the transducer pipe([filter(...), map(...)])
reducer
is an alias for reducing function, very much the same as the one supplied to reduce
y = reduce(reducer)(x)
A reducer takes two arguments: an aggregate y
and an iterative value xi
.
It can be something like (y, xi) => doSomethingWith(y, xi)
A transducer
is a function that takes a reducer and returns another reducer
transducer = reducer => (y, xi) => reducer(doSomethingWith(y, xi))
The transducer above, when passed a reducer, returns another reducer that will do something
with y
and xi
, then pass it to the input reducer
We can create a chained reducer by passing a reducer to a chain of transducers
Imagine dominos falling over. The reducer you pass to a chain of transducers is called last.
Because of this implementation detail,
if
x
is a function, pipe chainsfunctions
from right to left
You can use pipe
to construct chains of transducers. Pipe will read left to right in all cases.
There are two other functions you'll need to get started with transducers, map
and filter
.
given x
is a reducer, f
is a mapping function; map(f)(x)
is a transduced reducer
that applies f
to each element in the final transform pipeline.
given x
is a reducer, f
is a predicate function; filter(f)(x)
is a transduced reducer
that filters each element in the final transform pipeline based on f
The following transformations isOdd
, square
, and squaredOdds
are used as transducers
const concat = (y, xi) => y.concat([xi])
const isOdd = filter(x => x % 2 === 1)
transform(isOdd, [])([1, 2, 3, 4, 5]) // => [1, 3, 5]
reduce(
isOdd(concat),
[],
)([1, 2, 3, 4, 5]) // => [1, 3, 5]
const square = map(x => x ** 2)
transform(square, [])([1, 2, 3, 4, 5]) // => [1, 4, 9, 16, 25]
reduce(
square(concat),
[],
)([1, 2, 3, 4, 5]) // => [1, 4, 9, 16, 25]
const squaredOdds = pipe([isOdd, square])
transform(squaredOdds, [])([1, 2, 3, 4, 5]) // => [1, 9, 25]
reduce(
squaredOdds(concat),
[],
)([1, 2, 3, 4, 5]) // => [1, 9, 25]
The following transformations isOdd
, square
, and squaredOdds
are not used as transducers
const isOdd = filter(x => x % 2 === 1)
isOdd([1, 2, 3, 4, 5]) // => [1, 3, 5]
const square = map(x => x ** 2)
square([1, 2, 3, 4, 5]) // => [1, 4, 9, 16, 25]
const squaredOdds = pipe([isOdd, square])
squaredOdds([1, 2, 3, 4, 5]) // => [1, 9, 25]
Examples
https://deno.land/std/http serve
A webserver using map, transform, andimport { serve } from "https://deno.land/std/http/server.ts";
import { map, transform } from "https://deno.land/x/rubico/rubico.js"
const s = serve({ port: 8001 });
console.log("http://localhost:8001/");
transform(map(req => {
req.respond({ body: "Hello World\n" });
}), null)(s);
A server with middleware
import { serve } from 'https://deno.land/std/http/server.ts'
import {
pipe, fork, assign, tap, tryCatch, switchCase,
map, filter, reduce, transform,
any, all, and, or, not,
eq, gt, lt, gte, lte,
get, pick, omit,
} from 'https://deno.land/x/rubico/rubico.js'
const join = delim => x => x.join(delim)
const addServerTime = req => {
req.serverTime = (new Date()).toJSON()
return req
}
const traceRequest = pipe([
fork([
pipe([get('serverTime'), x => '[' + x + ']']),
get('method'),
get('url'),
]),
join(' '),
console.log,
])
const respondWithHelloWorld = req => {
req.respond({ body: 'Hello World\n' })
}
const respondWithServerTime = req => {
req.respond({ body: `The server time is ${req.serverTime}\n` })
}
const respondWithNotFound = req => {
req.respond({ body: 'Not Found\n' })
}
const route = switchCase([
eq('/', get('url')), respondWithHelloWorld,
eq('/time', get('url')), respondWithServerTime,
respondWithNotFound,
])
const onRequest = pipe([
addServerTime,
tap(traceRequest),
route,
])
const s = serve({ port: 8001 })
console.log('http://localhost:8001/')
transform(map(onRequest), null)(s)