NAV Navbar
  • Functional Promises
  • Thenable Methods
  •    Array Methods
  •    Errors
  •    Conditional
  •    Utilities
  •    Properties
  • Specialty Methods
  •    Helpers
  •    Events
  •    Composition Pipeline
  •    Modifiers
  • Misc
  • 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 with FP.thenIf() to handle response.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)
    

    Build Status GitHub package version GitHub stars

    NPM

    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.7.95 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 of thenable! It works because an Error in a Promise will cause it to skip or "surf" over thenables until it finds a special thenable: .catch(). It then takes that Error value and passes it into the function. .catch(err=>{log(err.message)})

    Thenable methods in FP 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.

    1. map
    2. filter
    3. find
    4. some
    5. 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!')}))
    

    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

       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:

    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:

    1. Create a chain, name it squareAndFormatDecimal.
    2. When squareAndFormatDecimal(nums) is passed an Array<Number> it must:
      1. Square each number.
      2. Convert each number to a decimal, then format with float.toFixed(2).
    3. 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:

    1. Define single-arg helper methods setStatus() & setError()
    2. Start chain expression
    3. Get element using .get() to extract target property (which will be a <form></form>)
    4. Get value from contained input.todo-text element
    5. Put todo's text into a JS Object shaped for service endpoint
    6. Pass data along to todoAPI.create() method
    7. Update UI with setStatus()
    8. 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 and update 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.


    Docs Powered by Slate

    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

    Thank you all for the support & encouragement!

    JavaScript community is the best!