petkaantonov / bluebird

:bird: :zap: Bluebird is a full featured promise library with unmatched performance.
http://bluebirdjs.com
MIT License
20.45k stars 2.33k forks source link

Feature Request: Branching construct #984

Closed r1b closed 8 years ago

r1b commented 8 years ago

Frequently I find myself wanting to add some control flow to my promise chains, e.g:

let startSomething = () => {
  // This promise resolves with a value & some set of conditions about what happens next
};

let aChain = doSomething().then(finishSomething);
let bChain = doSomethingElse.then(finishSomethingElse);
let cChain = doAnotherThing.then(finishAnotherThing);

// n = 2
// where predicateFn = (Any -> Boolean)
startSomething().then(Promise.decide(predicateFn, aChain, bChain))
// if predicateFn is true, we execute aChain with the value resolved from startSomething, 
// else we do the same with bChain

// For all n
// where predicateFnStar = (Any -> Listof[Boolean])
// Invariant: The list of booleans returned from predicateFnStar must not contain
// more than one true value
doSomething.then(Promise.select(predicateFnStar, aChain, bChain, cChain, ... , nChain))
// The structure of the array returned by predicateFnStar determines which of aChain,
// bChain, &c is executed with the value resolved from startSomething

Is this something you could see in bluebird?

Best, Rob

benjamingr commented 8 years ago

Promises are already started values. In your case, both aChain, bChain and cChain area already executing.

Control flow can be accomplished in a variety of ways, namely an if/else works :)

Celadora commented 7 years ago

Is this a feature you would consider were someone were to make a pull request? We have a solution that we'd like to offer.

benjamingr commented 7 years ago

Feel free to offer the solution, no guarantees

Celadora commented 7 years ago
Promise.prototype.branch = function(...fns) {
 return this.all().spread((index, ...values)=>{
  if(index===null ||
     index === undefined ||
     typeof fns[index] !== 'function') return Promise.reject(values);
  return fns[index].apply(this, values);
 })
}

new Promise.resolve().then(nothing=>{
  return [2, 'somevalue', 'anothervalue']
})
.branch(a, b, c, d)
.done(value=>{
  console.log(value);
})
function a(value) {
  console.log('a', value);
  return 0;
}
function b(value) {
  console.log('b', value);
  return 1;
}
function c(valuea, valueb) {
  console.log('c', valuea, valueb);
  return 2;
}
function d(value) {
  console.log('d', value);
  return 3;
}

It's used much like spread, in that you return an array from a previous link in the chain. The array is simple: [index, ...values]. The index corresponds to the branch, and the value is arbitrary.

spion commented 7 years ago

This seems like it would be better handled with a combinator function branch:

function branch(...fns) {
  return function([index, ...values]) {
    if (index === null ||
        index === undefined ||
        typeof fns[index] !== 'function') return Promise.reject(values);
    return fns[index].apply(this, values)
  }
}
import {branch} from 'promise-combinators'

new Promise.resolve().then(nothing=>{
  return [2, 'somevalue', 'anothervalue']
})
.then(branch(a, b, c, d))

The benefit is that it works with any promise library

edit: Tried to fix errors.

Celadora commented 7 years ago

I might be misunderstanding your solution, when I run it caseId is undefined.

I would prefer branch being used in place of then, but I can see how your solution can work, and has syntactic sugar.

spion commented 7 years ago

@Celadora Sorry, I forgot to separate the case id from the rest of the arguments.

Should be fixed now, using better variable names and rest arguments.

justsml commented 7 years ago

Hey @benjamingr @spion @r1b @Celadora I'd like to suggest an alternative pattern: .thenIf() (and related idea: .tapIf())

// Either use like so:
const checkEmail = email => Promise.resolve(email)
  .thenIf(e => e.length > 5)
// Or like so:
const checkEmail = email => Promise.resolve(email)
  .then(thenIf(e => e.length > 5))

function thenIf(cond, ifTrue = (x) => x, ifFalse = (x) => null) {
 return value => Promise
  .resolve(cond(value))
  .then(ans => ans ? ifTrue(value) : ifFalse(value))
 })
}
Promise.prototype.thenIf = thenIf;
justsml commented 7 years ago

Here's how this could be helpful refactoring the .props() docs example into something like:

function directorySizeInfo(dir) {
  const counts = {dirs: 0, files: 0, totalSize: 0, min: {size: Infinity}, max: {size: -Infinity}};
  const checkMin = s => counts.min = s.size > 0 && s.size < counts.min.size ? s : counts.min;
  const checkMax = s => counts.max = s.size > 0 && s.size > counts.max.size ? s : counts.max;
  const totalInc = s => counts.totalSize += s.size;

  return {
    files:      getStats(dir), 
    counts:     {dirs: counts.dirs, files: counts.files},
    smallest:   counts.min,
    largest:    counts.max,
    totalSize:  counts.totalSize,
  }

  function getStats(dir) {
    return fs.readdirAsync(dir)
    .map(fileName => {
      return Promise
      .resolve(path.join(dir, fileName))
      .then(fs.statAsync)
      .tap(s => [totalInc, checkMax, checkMin].map(fn => fn(s)))
      .thenIf(stat => stat.isDirectory(), 
        stat => {
          counts.dirs++;
          return getStats(stat.filePath);
        }, stat => {
          counts.files++;
          return stat;
        })
    }).then(_.flatten);
  }
}