rubico
🏞 a shallow river in northeastern Italy, just south of Ravenna
[a]synchronous functional syntax
import { pipe, map, filter } from 'rubico'
const isOdd = x => x % 2 === 1
const square = x => x ** 2
pipe([
filter(isOdd),
map(square),
])([1, 2, 3, 4, 5]) // [1, 9, 25]
Motivation
A note from the author
At a certain point in my career, I grew frustrated with the entanglement of my own code. While looking for something better, I found functional programming. I was excited by the idea of functional composition, but disillusioned by the complex hierarchy of effectful types. I started rubico to capitalize on the prior while rebuking the latter. Many iterations since then, the library has grown into something I personally enjoy using, and continue to use to this day.
rubico's value resides at the intersection of the following principles:
- asynchronous code should be simple
- functional style should not care about async
- functional transformations should be composable, performant, and simple to express
When you use this library, you obtain the freedom that comes only from having those three points fulfilled. The result is something you may enjoy.
Introduction
rubico is a robust, highly optimized syntax for async agnostic functional programming in JavaScript. The style and naming of the syntax is idiomatic across languages and other libraries; using this library should feel second nature. Just like regular vanilla JavaScript syntax and operators, rubico operates predictably on vanilla JavaScript types. When you use this library, you can stop worrying about the complex fluff of Promise management. When something goes wrong, rubico throws meaningful and ergonomic errors. You should use this library if you want to become a better programmer, write cleaner and more concise code, or harness the expressive power of functional programming in production.
Here are my recommendations for getting started.
- check out the docs
- take the tour
- use rubico in a project
- at your leisure, peruse the awesome resources
- help with rubico
API
const {
pipe, fork, assign,
tap, tryCatch, switchCase,
map, filter, reduce, transform, flatMap,
any, all, and, or, not,
eq, gt, lt, gte, lte,
get, pick, omit,
} = rubico
Installation
with npm
npm i rubico
browser script, global rubico
<script src="https://unpkg.com/rubico"></script>
with deno
import rubico from 'https://deno.land/x/rubico/rubico.js'
System Requirements
- minimum node version: 10.3
- minimum Chrome version: 63
- minimum Firefox version: 57
- minimum Edge version: 79
- minimum Safari version: 11.1
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]
Awesome Resources
Beginner
- Practical Functional Programming in JavaScript - Why it's worth it
- Practical Functional Programming in JavaScript - Data last
- Practical Functional Programming in JavaScript - Side Effects and Purity
- Practical Functional Programming in JavaScript - Intro to Transformation
Advanced
Contributing
Your feedback and contributions are welcome. If you have a suggestion, please raise an issue. Prior to that, make sure to search through the issues first in case your suggestion has been made already. If you decide to work on an issue, please announce on the issue thread that you will work on it.
Enhancements should follow the principles of the library:
- asynchronous code should be simple
- functional style should not care about async
- functional transformations should be composable, performant, and simple to express
Pull requests should provide some basic context and link the relevant issue. My intention is that progress on the library follow an issue -> pull request format. See this pull request for an example.
If you are interested in contributing, the help wanted tag is a good place to start.
License
rubico is MIT Licensed.