cujojs / when

A solid, fast Promises/A+ and when() implementation, plus other async goodies.
Other
3.44k stars 396 forks source link

Potential convenience functions? #385

Closed brandoncarl closed 9 years ago

brandoncarl commented 9 years ago

Hi – I wanted to open a dialog about a few convenience functions to make promises more "functional". We can separate into separate issues if fitting. Not firm whatsoever on names, just wanted to start a dialog.

The first functions expand upon cases of fold and act as convenience functions for promise "chains". I would suggest prepend, append and set.

prepend and append would be used for "passing" arrays through the chain. This would be as opposed to creating global scopes to pass variables across functions.

// Current chain
task1()

.then(function(val1) {
  // Suppose that task2 either depends on val1, or should not run in the event that task1 fails
  return task2(val1).fold(function(x,y) { return [x,y] }, val1);
})

.spread(function(val1, val2) {
  // Logic here
});

// Proposed chain
task1()

.then(function(val1) {
  return task2(val1).prepend(val1);
})

.spread(function(val1, val2) {
  // Logic here
});

Meanwhile, set would be used for assembling an object.

task1()

.then(function(val1) {
  // Suppose that val1 is an object such as { a : "hi" }. If task2 promise resolved to "there"
  // the end result would be a promise yield the object { a : "hi", b : "there" }
  return task2(val1).set("b", val1);
});

Lastly, I am finding that the spread function is tempting me to write functions that return arrays (which I tend to find as a less desirable practice than return objects). Functions that return objects tend to be more flexible across use cases, as they don't determine the order of subsequent functions. I think that, taking a cue from Lodash/Underscore, a pick function could serve as a nice intermediary between functions that return objects, and "spread".

For example:

  P1 = owe({ a : "hi", b : "there" })
  P1.pick(["a", "b"]).spread(function(a, b) { ... });

Apologies for the long post – given that these are all somewhat "functional" in nature, I wanted to keep the initial dialog cohesive. If there are convenient ways of achieving these results, please do let me know!

briancavalier commented 9 years ago

Hey @brandoncarl. Yes, that's quite a lot of info :) no worries, though, all good stuff to discuss.

I'll say up front that my general philosophy right now is to try to keep when.js's API as lean as possible as we head toward 4.0, by providing the most essential building blocks, like then, fold, try, lift, etc., rather than attempt to provide all the utilities that can possibly be built from them. Devs all have their own approach to solving problems and use promises is a wide variety of ways, so trying to provide all possible utility methods is a slippery slope.

That said, I think there are ways to achieve the things you mention using existing APIs.

In your first example, it looks like the ultimate goal is to execute a binary function that looks like:

function computeResult(val1, val2) { ... }

With the additional constraint that val1 and val2 should be computed serially (and val2 should not be computed if the task computing val1 fails).

Here's a fairly simple way to do that, without using spread.

var val1 = task1();
var val2 = val1.then(task2);
var result = when.try(computeResult, val1, val2);

In that case, if task1 fails, task2 will not execute. Similarly, computeResult will not execute if either task1 or task2 fails. I think I got that right, but if it's not the same as what you're after, let me know where I goofed, and I'll try to get it right.

For the second and third example, there was actually a similar discussion over at cujojs/most very recently. It's pretty simple to write a reusable set function:

// Write a simple (ie not promise-aware) set function
function set(k) {
    return function(value, target) {
        target[k] = value;
        return target;
    };
}

// Use it with promise.fold
// Incidentally, val1 can be a promise or a non-promise value in this case
var promiseForUpdatedObject = promiseForObject.fold(set("b"), val1);

And a reusable pick function that will work with Arrays, Promises, reactive streams, and really anything that provides a "map-like" operation.

// Write a simple (ie not promise-aware) pick function
function pick(keys) {
    return function(obj) {
        // Array.map, or use lodash if you prefer
        return keys.map(function(k) {
            return x[k];
        });
    };
}

// Use it with promises:
var promise = when.resolve({ a : "hi", b : "there" })

promise.then(pick(["a", "b"])).spread(...);

// Or with an array
var arrayOfPairs = arrayOfObjects.map(pick(["a", "b"]));

// Or a reactive stream
var most = require('most');
var stream = most.create(...);
stream.map(pick(["a", "b"])).observe(console.log.bind(console));

Does that help?

briancavalier commented 9 years ago

@brandoncarl Ping. Any thoughts on the approaches above?

brandoncarl commented 9 years ago

Hey Brian sorry for the delay. We went into production crunch. This is a great answer and I'll be back but closing for now. Thanks for the response!

On Friday, November 7, 2014 at 8:02 AM, Brian Cavalier wrote:

@brandoncarl (https://github.com/brandoncarl) Ping. Any thoughts on the approaches above?

— Reply to this email directly or view it on GitHub (https://github.com/cujojs/when/issues/385#issuecomment-62139002).

briancavalier commented 9 years ago

Cool, no problem. Hope all went well with the production release :)