Functional Promises
# Install
npm install functional-promises
//// Import into your app:
const FP = require('functional-promises')
// OR:
import FP from 'functional-promises'
Star Functional Promises
on Github
Examples & Awesome Shit
Array-style methods are built-in:
FP.resolve(['1', '2', '3', '4', '5'])
.map(x => parseInt(x))
.filter(x => x % 2 === 0)
.then(results => {
console.log(results) // [2, 4
})
Create re-usable sequences of functions with
.chain()
.
const squareAndFormatDecimal = FP
.chain()
.concurrency(4)
.map(x => x * x)
.concurrency(2)
.map(x => parseFloat(x).toFixed(2))
.chainEnd() // returns function
squareAndFormatDecimal([5, 10, 20])
.then(num => console.log(num)) // ['25.00', '100.00', '400.00']
Use
fetch
withFP.thenIf()
to handleresponse.ok === false
with custom response.
//// Wrap `fetch()` with `FP.resolve()` to use `FP`'s methods
FP.resolve(fetch('/profile', {method: 'GET'}))
.thenIf( // thenIf lets us handle branching logic
res => res.ok, // Check if response is ok
res => res.json(), // if true, return the parsed body
res => ({avatar: '/no-photo.svg'})) // fail, use default object
.get('avatar') // Get the resulting objects `avatar` value
.then(avatarUrl => imgElement.src = avatarUrl)
Summary
The Functional Promises library is a Fluent Function Chaining Interface and Pattern.
Core features: Array Methods, Events, Array AND Object FP.all()
Resolution, Re-usable Function Chains, Conditional/Branching Logic, Concurrency, Smart Error Handling.
FP
features seamless support between synchronous code, async
/await
, and native Promises. The core Functional Composition is powered by the FP.chain()
construct.
Why not simply use [library X]?
FP
's un-minified source is only ~370 lines of code.
The compressed+minified bundle weighs in at a humble ~3Kb. The non-gzipped bundle weighs in around 10Kb (using Webpack+Babel+Rollup+UglifyJS).
Library Comparison
Library | Main deal | Files | Lines of Code | .min.js kB |
---|---|---|---|---|
Functional Promise v1.8.1 | Sync & Async Chains | 8 | 376 | 10 Kb / 3 Kb gzipped |
Bluebird v3.5.1 | Promises Replacement | 38 | 5,188 | 80 Kb |
RxJS v5.5.6 | Observables Chaining | 458 | 12,266 | 150 Kb |
IxJS v2.3.4 | [Async]Iterable Chaining | 521 | 12,366 | 145 Kb |
FP
is roughly 1/30th the lines of code in IxJs
. And it's bundle size is about 1/9th the size! However IxJS
/RxJS
features a far larger API with 100's of methods.
BluebirdJS and FP have roughly the same number (and type) of API methods, yet FP
is far less code.
To be clear: Bluebird, RxJS and IxJS are amazing. Their patterns have been very influential on FP
's design.
Note: R/IxJS
's modular design also allows for bundle sizes to be smaller (using different syntax).
API Outline
All .then()
-powered methods are listed first.
Thenable Methods
A
.catch()
is another type ofthenable
! It works because an Error in a Promise will cause it to skip or "surf" overthenables
until it finds a specialthenable
:.catch()
. It then takes that Error value and passes it into the function..catch(err=>{log(err.message)})
Thenable
methods inFP
include: Arrays, Errors, Conditional, Utilities, Properties, etc.
Most FP
methods derive behavior from Native Promise's .then()
.
For example, .tap(fn)
's function will receive the resolved value exactly like a .then()
. Except the function's return
value will be ignored - and the next thenable
in the chain will get the original input.
Array Methods
const rawData = [-99, null, undefined, NaN, 0, '99']
// Async compatible (not needed in this simple example)
FP.resolve(rawData)
.filter(x => x) // truthiness check = [-99, "99"]
.map(x => parseInt(x)) // convert to numeric [-99 99]
.find(n => n >= 1) // is gte 1, idx = 1
.then(num => {
console.log(num) // 99
})
Reuse functions, e.g. Native Array methods:
const rawData = [-99, null, undefined, NaN, 0, '99']
// ... Compare w/ Native Array Method Usage:
rawData
.filter(x => x) // truthiness check = [-99, "99"]
.map(x => parseInt(x)) // convert to numeric [-99, 99]
.find(n => n >= 1)
Any .then()
which would handle an array, may instead use one of the FP
array methods.
- map
- filter
- find
- some
- none
FP.map(iterable, fn)
FP.resolve([1, 2, 3, 4, 5])
// Native es6 example: (Synchronous)
//.then(nums => nums.map(x => x * 2))
.map(x => x * 2)
.then(results => {
console.log(results) // [2, 4, 6, 8, 10]
})
Similar to Array.prototype.map((item[, index, array]) => {})
.
Use to transforms an array of values, passing each through the given function.
The return value will be a new array containing the result for each call to fn(item)
.
For example, let's say you have to multiply a list of numbers by 2.
Using FP.map()
to do this lets you focus on the important logic: x => x * 2
Another neat trick w/
FP
is auto-resolving nested Promises. Now you can ignore finickey details, like when AJAX data will be available.
const dumbPromises = [Promise.resolve(25), Promise.resolve(50)]
FP.resolve(dumbPromises)
.concurrency(1)
.map(num => FP.resolve(num).delay(num))
.then(msec => `Delayed ${msec}`)
.then(results => console.log(results))
FP.filter(iterable, fn)
FP.resolve([1, null, 3, null, 5])
.filter(Boolean)
// Or similarly:
// .filter(value => value ? true : false)
.then(results => console.log(results)) // [1, 3, 5]
Use .filter()
to omit items from the input array by passing through a given function. Items will be omitted if the function returns a falsey value.
FP.find(iterable, fn)
FP.resolve([1, 2, 3, 4, 5])
.find(x => x % 2 === 0)
.then(results => {
console.log(results) // 2
})
Returns first item to return truthy for fn(item)
If no match is found it will return undefined
.
FP.some(iterable, fn)
FP.resolve([1, 2, 4])
.some(x => x % 2 === 0)
.then(results => {
console.log(results, true)
})
Returns Promise<true>
on the first item to return truthy for fn(item)
If no truthy result is found, .some()
returns Promise<false>
.
FP.none(iterable, fn)
FP.resolve([1, 2, 4])
.none(x => x % 2 === 0)
.then(results => {
console.log(results) // false
})
.none()
resolves to Promise<false>
on the first item to return falsey for fn(item)
If no match is found it will return Promise<true>
.
Errors
FP.catch(fn)
FP.catchIf(type, fn)
Catching errors by type
FP.resolve()
.then(() => {
throw new TypeError('Oh noes')
})
.then(() => console.error('must skip this!'))
.catch(ReferenceError, () => console.error('arg too specific for .catch(type)'))
.catch(SyntaxError, () => console.error('arg too specific for .catch(type)'))
.catch(TypeError, err => console.info('Success!!! filtered .catch(type)', err))
.catch(err => console.error('Fallback, no error type matched'))
.catch()
is analgous to native Promise error handling.
This example uses TypeError
matching to print the 'success' message - ignoring the other catch
's.
Conditional
FP.thenIf()
Email 'validator'
let email = 'dan@danlevy.net'
FP.resolve(email)
.thenIf(
e => e.length > 5, // Conditional
e => console.log('Valid: ', e), // ifTrue
e => console.error('Bad Email: ', e)) // ifFalse
.thenIf(condition(value), ifTrue(value), ifFalse(value))
Arguments
Functional Promise Login Flow
//// Check if login successful, returning a token:
const authUser = (email, pass) => FP
.resolve({email, pass})
.then(({email, pass}) => svc.loginGetUser(email, pass))
.thenIf(
user => user.token, // is valid login
user => user, // return user to next .then function
() => {throw new Error('Login Failed!')}))
condition
, echo/truthy function:(x) => x
ifTrue
, echo function:(x) => x
ifFalse
, quiet function:() => null
The condition
function should return either true
/false
or a promise that resolves to something true
/false
.
ifTrue
function is called if the condition
resulted in a truthy value. Conversely, ifFalse
will be called if we got a false answer.
The return value of either ifTrue
/ifFalse
handler will be handed to the next .then()
.
Default values let you call .thenIf
with no args - if you simply want to exclude falsey values down the chain.
Utilities
FP.tap(fn)
FP.resolve(fetch('http://jsonplaceholder.typicode.com/photos/11'))
.tap(res => console.log(`ok:${res.ok}`))
.then(res => res.json())
.tap(data => console.log('Keys: ' + Object.keys(data).sort().join(',')))
.then(data => `<img src='${data.url}' alt='${data.title}' />`)
.tap(data => console.log('Image Url: ' + data.url))
FP.resolve(fetch('https://api.github.com/users/justsml'))
.tap(res => console.log(`github user req ok? ${res.ok}`))
.then(res => res.json())
.tap(data => console.log('Keys:', Object.keys(data)))
.then(data => console.log(data))
The .tap()
method is FP
's primary way to use the familiar console.log()
- know it well.
It works just like .then()
except it's return value is ignored. The next thenable
will get the same input.
Perfect for logging or other background tasks (where results don't need to block).
FP.delay(ms)
Delay per-array item.
const started = Date.now()
FP.resolve([1, 2, 3, 4])
.concurrency(1)
// now only 1 map() callback happens at a time
.map(num => {
return FP
.delay(50)
.resolve(num)
})
.then(() => {
const runtime = Date.now() - started
console.log(`Delayed ${runtime}ms.`)
console.log(`Success: ${runtime >= 200}`)
})
Single delay added mid-sequence.
const started = Date.now()
FP.resolve([1, 2, 3])
.delay(250)
.map(num => num + num)
.then(() => {
const runtime = Date.now() - started
console.log(`Delayed ${runtime}ms.`)
console.log(`Success: ${runtime >= 250}`)
})
.delay(milliseconds)
is a helpful utility. It can help you avoid exceeding rate-limits in APIs. You can also use it to for simulated bottlenecks, adding 'slowdowns' exactly where needed can greatly assist in locating many kinds of complex bugs.
Usage
- Shorthand with static helper:
.then(FP.delay(waitMs))
- Nesting with static helper: FP.delay(waitMs):
.then(num => FP.delay(waitMs).then(() => num))
- Using FP's instance method.
FP.resolve([1, 2, 3]).delay(250)
Properties
These methods are particularly helpful for dealing with data extraction/transformation.
FP.get(keyName)
FP.resolve({foo: 42})
.get('foo')
.then(x => {
console.log(x) // x === 42
})
Use to get a single key's value from an object.
Returns the key value.
FP.set(keyName, value)
A common use-case includes dropping passwords or tokens.
FP.resolve({username: 'dan', password: 'sekret'})
.set('password', undefined)
.then(obj => {
console.log(obj.password) // obj.password === undefined
})
Use to set a single key's value on an object.
Returns the modified object.
Specialty Methods
Helpers
FP.promisify(function)
//// fs - file system module
const fs = require('fs')
const readFileAsync = FP.promisify(fs.readFile)
readFileAsync('/tmp/test.csv', 'utf8')
.then(data => console.log(data))
Utility to get a Promise-enabled version of any NodeJS-style callback function (err, result)
.
FP.promisifyAll()
//// Common promisifyAll Examples:
// fs - node's file system module
const fs = FP.promisifyAll(require('fs'))
/* USAGE:
fs.readFileAsync('/tmp/test.csv', 'utf8')
.then(data => data.split('\n'))
.map(line => line.split(','))
.then(renderTable)
*/
// Redis
const redis = require('redis')
// FP.promisifyAll(redis) // 💩 wont work
FP.promisifyAll(redis.RedisClient.prototype) // 👍
FP.promisifyAll(redis.Multi.prototype) // 👍
/* USAGE:
client.getAsync('foo')
.then(data => console.log('results', data)) */
// Mongodb (Note: use Monk, or Mongoose w/ native Promise support)
const MongoClient = require('mongodb').MongoClient;
FP.promisifyAll(MongoClient)
/* USAGE:
MongoClient.connectAsync('mongodb://localhost:27017')
.then(db => db.collection('documents')) // get collection
.then(FP.promisifyAll) // check to make sure we can use *Async methods
.then(db => db.findAsync({})) // query w/ findAsync
.catch(err => console.error('mongodb failed', err)) */
// mysql - Note: that mysql's classes are not properties of the main export
// Here's another way to `promisifyAll` prototypes directly
FP.promisifyAll(require('mysql/lib/Connection').prototype)
FP.promisifyAll(require('mysql/lib/Pool').prototype)
// pg - Note: postgres client is same as `node-postgres`
// - and pg supports promises natively now!
// Mongoose
const mongoose = FP.promisifyAll(require('mongoose'))
/* USAGE:
mongoose.Promise = FP
model.findAsync({})
.then(results => {...}) */
// Request
FP.promisifyAll(require('request'))
/* USAGE:
request.getAsync(url)
request.postAsync(url, data)
// requestAsync(..) // will not return a promise */
// rimraf - The module is a single function, use `FP.promisify`
const rimrafAsync = Promise.promisify(require('rimraf'))
// Nodemailer
FP.promisifyAll(require('nodemailer'))
// xml2js
FP.promisifyAll(require('xml2js'))
FP.promisifyAll(Object/Class/Prototype)
accepts an Object/Class/Prototype-based-thing
and for every key of type function
it adds a promisified version using the naming convention obj.[functionName]Async()
.
Compared to bluebird
, FP added a few tweaks to make it more versatile, specifically it works on any object - not limited to Classes and functions w/ a prototype
.
promisifyAll
is inspired by Bluebird's API.
//// edge case:
const AwkwardLib = require("...")
const tmpInstance = AwkwardLib.createInstance()
FP.promisifyAll(Object.getPrototypeOf(tmpInstance))
// All new instances (incl tmpInstance) will feature .*Async() methods
In all of the above cases the library made its classes available in one way or another. If this is not the case (factory functions, et al.), you can still promisify by creating a throwaway instance:
FP.resolve(<anything>)
Promise anything like it's going out of style:
FP.resolve()
FP.resolve(42)
FP.resolve(fetch(url))
FP.resolve(Promise.resolve(anything))
Turn anything into a Functional Promise
wrapped promise!
Use to convert any Promise-like interface into an FP
.
FP.all()
FP.all([
Promise.resolve(1),
Promise.resolve(2)
])
.then(results => console.log(results))
FP.all({
one: Promise.resolve(1),
two: Promise.resolve(2)
})
.then(results => console.log(results))
FP.all()
provides an extended utility above the native Promise.all()
, supporting both Objects and Arrays.
Note: Non-recursive.
FP.unpack()
function edgeCase() {
const { promise, resolve, reject } = FP.unpack()
setTimeout(() => resolve('All done!'), 1000)
return promise
}
edgeCase()
.then(result => console.log(result))
Use sparingly. Stream & event handling are exempt from this 'rule'. If using ES2015, destructuring helps to (more cleanly) achieve what deferred
attempts.
deferred
is an anti-pattern because it doesn't align well with Functional Composition.
Events
//// Example DOM code:
const button = document.getElementById('submitBtn')
FP.chain()
.get('target')
.then(element => element.textContent = 'Clicked!')
.listen(button, 'click')
Key considerations:
Let's start with their similarity, both are (essentially) async...
And now for some differences:
- Promises are single-execution cached values. Memoized. Events can run many times per second with different arguments or data.
- Events have control flow to think about (
e.preventDefault()
). Promises flow in one direction. - Promises depend on
return
's everywhere. Event handlers whichreturn
may cause unexpected control-flow issues.
Yikes.
Let's look at some code & see how FP
improves the situation:
FP.listen()
event helper
FP.chain()
.get('target')
.set('textContent', 'Clicked!')
.listen(button, 'click')
The .listen()
method must be called after an FP.chain()
sequence of FP
methods.
Note: The .chainEnd()
method is automatically called.
Composition Pipeline
Composition Pipelines is a combination of ideas from Collection
Pipeline and Functional Composition.
Chained Functional Promises unlock a powerful technique: Reusable Async Composition Pipeline.
Enough jargon! Let's create some slick JavaScript:
FP.chain() / .chainEnd()
The method FP.chain()
starts 'recording' your functional chain.
All chain-based features (FP.listen(el, ...events)
, FP.run(opts)
, et. al.) use .chainEnd()
to get a function to 'replay' the methods after .chain()
.
Whether directly or indirectly .chainEnd()
must be called.
const getTarget = FP
.chain()
.get('target')
.chainEnd()
const handler = event => getTarget(event)
.then(target => {console.log('Event target: ', target)})
FP.chain()
is a static method on FP
.
Re-usable Promise Chains
const squareAndFormatDecimal = FP
.chain()
.map(x => x * x)
.map(x => parseFloat(x).toFixed(2))
.chainEnd()
squareAndFormatDecimal([5, 6])
.then(num => console.log(num)) // ['25.00', '36.00']
HOW TO: Create a re-usable chain with 2 .map
steps:
- Create a chain, name it
squareAndFormatDecimal
. - When
squareAndFormatDecimal(nums)
is passed anArray<Number>
it must:- Square each number.
- Convert each number to a decimal, then format with
float.toFixed(2)
.
- Execute named function
squareAndFormatDecimal
with array[5, 6]
.
Events + Promise Chain
//// Example DOM Code
const form = document.querySelector('form')
const submitHandler = createTodoHandler()
form.addEventListener('submit', submitHandler)
function createTodoHandler() {
const statusLbl = document.querySelector('label.status')
const setStatus = s => statusLbl.textContent = s
const setError = err => setStatus(`ERROR: ${err}`)
return FP
.chain() // input arg will get 'passed' in here
.get('target')
.then(form => form.querySelector('input.todo-text').value)
.then(todoText => ({id: null, complete: false, text: todoText}))
.then(todoAPI.create)
.tap(createResult => setStatus(createResult.message))
.catch(setError)
.chainEnd()
}
The method createTodoHandler()
gives you a Functional chain to:
- Define single-arg helper methods
setStatus()
&setError()
- Start chain expression
- Get element using
.get()
to extracttarget
property (which will be a<form></form>
) - Get value from contained
input.todo-text
element - Put todo's text into a JS Object shaped for service endpoint
- Pass data along to
todoAPI.create()
method - Update UI with
setStatus()
- Handle any errors w/
setError()
Controller + Events + Promise Chain
Usage Example: (see Class implementation below)
//// usage example - standard promise code:
const todoApp = TodoApp()
todoApp.update({id: 1, text: 'updated item', complete: true})
.then(console.warn.bind(console, 'update response:'))
todoApp.add('new item')
.then(result => {
console.log('Added item', result)
})
TodoApp will return an object with
add
andupdate
methods - based on FP.chain()
//// example code:
function TodoApp() {
const statusLbl = document.querySelector('label.status')
const setStatus = s => statusLbl.textContent = s
return {
add: FP.chain()
.then(input => ({text: input, complete: false}))
.then(todoAPI.create)
.tap(createResult => setStatus(createResult.message))
.chainEnd(),
update: FP.chain()
// in v1.5.0: .get('id', 'completed', 'text') // or:
.then(input => {
const {id, complete, text} = input
return {id, complete, text}
})
.then(todoAPI.update)
.tap(updateResult => setStatus(updateResult.message))
.chainEnd()
}
}
Example OOP style 'class' object/interface.
Here we implement the interface { add(item), update(item) }
using chained function expressions. It's implementation is hidden from the calling code.
This is a key differentiator between functional-promises
and other chaining libraries. No lockin.
Modifiers
FP.quiet()
FP.resolve([2, 1, 0])
.quiet()
.map(x => 2 / x)
.then(results => {
console.log(results) // [1, 2, Error])
})
Suppresses errors by converting them to return values.
Only applies to subsequent Array thenables.
FP.concurrency(threadLimit)
FP.resolve([1, 2, 3, 4, 5])
.concurrency(2)
.map(x => x * 2)
.then(results => {
console.log(results)// [2, 4, 6, 8, 10]
})
Set threadLimit
to constrain the amount of simultaneous tasks/promises can run.
Only applies to subsequent thenable Array methods.
Thanks to several influencial projects: RxJS, IxJS, Bluebird, asynquence, FantasyLand, Gulp, HighlandJS, et al.
Misc
Detailed Stats
./functional-promise.min.js
Compression Results
Utility | File Size |
---|---|
original | 17K |
gzip | 4.5K (3.69 X smaller) |
brotli | 4.0K (4.13 X smaller) |
./Rx.min.js
Compression Results
Utility | File Size |
---|---|
original | 146K |
gzip | 32K (4.68 X smaller) |
brotli | 27K (5.47 X smaller) |
./Ix.min.js
Compression Results
Utility | File Size |
---|---|
original | 129K |
gzip | 22K (6.01 X smaller) |
brotli | 17K (7.92 X smaller) |
Feedback
and other kind words
- "Small is beautiful." - Brendan Eich - co-inventor of JavaScript
- "Duuuuude!" - Sarah Drasner
- "nice, impressive!" - Kyle Simpson
- "This is really cool. Well done Dan" - Jamon Holmgren
- "quick first impression: they look amazing" - Brian Leroux
- "Oh wow, really like that interface!" - Robert Lord
- "That's so damn cool!!" - Alfie John
- "... At a quick glance this looks lovely. Blows my mind it can be so small." - Steve Kellock