Bluebird: Proposal: promise.if/when

Created on 29 Mar 2016  路  6Comments  路  Source: petkaantonov/bluebird

Motivation

I often find myself and other doing conditional chaining, where I create a initial promise and then keep conditionally adding additional actions to it.
For me personally this is a somewhat tiring task, it also feels hard to write and to read, therefor I decided to write something to make my live easier.

It supports _instant_ conditional chaining eg. perform the chaining directly when the chain is created and _deferred_ conditional chaining check the condition when the previous promise resolved.

A simple implementation and example usage would be as follows:

'use strict';

const Promise = require('bluebird');


function utilCallIfFuncOrReturn (func, val) {
   if (typeof func === 'function') {
      return func(val);
   }
   return func;
}

/**
 * Conditionally chains promises.
 * @param  {Function|* => *} predicate If function, the predicate will be invoked with the resolve
 * value of the previous promise.
 * If anything else, the truthness of the value will be used to immediately decide when to chain
 * the promises or not.
 * 
 * @param {Function|* => Promise|*} truthy If function, thruthy will be invoked with the resolved value of the
 * previous promise, the returned value will be inserted into the promise chain.
 * If anything else, the value will be inserted into the promise chain.
 * This will only be invoked if the predicate was truthy.
 * 
 * @param  {Function|* => Promise|*} falsy If function, falsy will be invoked with the resolved value of the
 * previous promise, the returned value will be inserted into the promise chain.
 * If anything else, the value will be inserted into the promise chain.
 * This will only be invoked if the predicate was false.
 * @return {Promise.<*>}
 */
Promise.prototype.if = Promise.prototype.when = function (predicate, truthy, falsy) {
   if (typeof predicate === 'function') {
      return this.then(val => {
         if (predicate(val)) {
            return utilCallIfFuncOrReturn(truthy, val);
         }
         if (falsy) {
            return utilCallIfFuncOrReturn(falsy, val);
         }
         return val;
      })
   } else if (predicate) {
      return this.then(val => utilCallIfFuncOrReturn(truthy, val));
   } else if (falsy) {
      return this.then(val => utilCallIfFuncOrReturn(falsy, val));
   }
   return this;
}

function someThingConditionally (val) {
   return val + 1;
}

function someThingWithDeferredConditionCheck (val) {
   return val - 1;
}

{
   const a = 1;
   const chain = Promise.resolve(1)
   .if(a === 1, someThingConditionally)
   .if(val => val % 2 === 0, someThingWithDeferredConditionCheck)
   .if(val => val === 3, () => { throw Error('I shouldn\'t be running'); })
   .then(val => console.log(1, val))
}

// vs

{
   const a = 1;
   let chain = Promise.resolve(1)
   if (a === 1) {
      chain = chain.then(someThingConditionally)
   }
   chain = chain.then(val => {
      if (val % 2 === 0) {
         return someThingWithDeferredConditionCheck(val);
      }
      return val;
   })
   chain = chain.then(val => {
      if (val === 3) {
         throw Error('I shouldn\'t be running');
      }
      return val;
   })
   .then(val => console.log(2, val))
}

As you can see the code is significantly more compact and (personally) easier to read/comprehend, there is also no significant performance overhead.

About the method name

I personally prefer if as it is very concise, but when would also be acceptable.

3.0.0 enhancement

Most helpful comment

Checking the condition before the promise is resolved is confusing and error-prone

Depends entirely on the use case, both moments might be appropriate. But checking in before is just as easy:

Promise.resolve(1)
     .then(a === 1 ? someThingConditionally : null)
     .then(val => val % 2 === 0 ? someThingWithDeferredConditionCheck(val) : val)
     .then(val => val === 3 ? Promise.reject(Error('I shouldn\'t be running')) : val)
     .then(val => console.log(2, val))

All 6 comments

There is also a third alternative:

   const a = 1;
   let chain = Promise.resolve(1)
   if (a === 1) { chain = chain.then(someThingConditionally) }
   chain = chain
     .then(val => val % 2 === 0 ? someThingWithDeferredConditionCheck(val) : val)
     .then(val => val === 3     ? Promise.reject(Error('I shouldn\'t be running')) : val)
     .then(val => console.log(2, val))
}

Checking the condition before the promise is resolved is confusing and error-prone, so if we leave that out:

   const a = 1;
   const chain = Promise.resolve(1)
     .then(val => a === 1       ? someThingConditionally(val) : val)
     .then(val => val % 2 === 0 ? someThingWithDeferredConditionCheck(val) : val)
     .then(val => val === 3     ? Promise.reject(Error('I shouldn\'t be running')) : val)
     .then(val => console.log(2, val))

Alternative implementation, using a combinator

let when = (condition, handle, handleElse) => val => {
  if (typeof condition === 'function') { condition = condition(val); }
  if (condition) { return handle(val); } 
  if (handleElse != null) { return handleElse(val); } 
  return val;
}

Usage of alternative implementation:

   const a = 1;
   const chain = Promise.resolve(1)
   .then(when(a === 1, someThingConditionally))
   .then(when(val => val % 2 === 0, someThingWithDeferredConditionCheck))
   .then(when(val => val === 3, () => { throw Error('I shouldn\'t be running'); }))
   .then(val => console.log(1, val))

Checking the condition before the promise is resolved is confusing and error-prone

Depends entirely on the use case, both moments might be appropriate. But checking in before is just as easy:

Promise.resolve(1)
     .then(a === 1 ? someThingConditionally : null)
     .then(val => val % 2 === 0 ? someThingWithDeferredConditionCheck(val) : val)
     .then(val => val === 3 ? Promise.reject(Error('I shouldn\'t be running')) : val)
     .then(val => console.log(2, val))

I say we vote. I vote no. @petkaantonov @spion ?

I am -1 as well

Afraid to say I'm against it as well: low overhead elimination compared to the most compact alternative
(the ternary operator version), is also easily implemented as a combinator.

Sorry

Was this page helpful?
0 / 5 - 0 ratings