mudge / pacta

An algebraic implementation of ECMAScript 2015 and Promises/A+ Promises in JavaScript for as many browsers and Node.js versions as possible
https://npmjs.org/package/pacta
BSD 3-Clause "New" or "Revised" License
71 stars 6 forks source link

Propagation of Promise state when concatenating #2

Closed mudge closed 11 years ago

mudge commented 11 years ago

As raised by @evilhackerdude in https://twitter.com/evilhackerdude/status/345660120874758145, should concatenating promises cause the resulting promise to inherit the state of the original promises (e.g. should a rejection in one of the promises cause the new promise to be rejected)?

var x = Promise.of([1]),
    y = Promise.of([2]),
    z = x.concat(y);

x.reject('Something went wrong.');

z.onRejected(function (reason) {
    console.log('Should this fire?');
});
seidtgeist commented 11 years ago

Wondering if @puffnfresh and @raganwald have something to add.

puffnfresh commented 11 years ago

I vote yes. Makes it something like the common Semigroup for Either. Which goes something like this:

class Semigroup a where
    concat :: a -> a -> a

instance Semigroup r => Semigroup Either l r where
    concat (Right a) (Right b) = Right (concat a b)
    concat (Left l) _ = Left l
    concat _ (Left l) = Left l
mudge commented 11 years ago

One thing that might be tricky is if both promises are rejected:

var z = x.concat(y);

x.reject('Foo');
y.reject('Bar');

z.onRejected(function (reason) {
    console.log('What is', reason, '?');
});

Should it fire with both reasons? If so, that violates the idea that promises can only be moved into a fulfilled or rejected state once (subsequent calls are idempotent).

mudge commented 11 years ago

The alternative here is that concatenating promises returns a new promise containing only the concatenation of their values and doesn't bring across state at all.

var z = x.concat(y);

x.reject('Foo');
y.reject('Bar');
z.reject('Baz');

z.onRejected(function (reason) {
    console.log('I am fired with Baz only.');
});

But this does feel a little odd. You could argue that concat is returning a wholly new promise disconnected from the original x and y but this behaviour is surprising: if x fails, for example, you'd want to know even if you only interrogate z.

puffnfresh commented 11 years ago

@mudge see the Haskell above. Either errors take the first error (i.e. x concat y == x, if x is an error).

seidtgeist commented 11 years ago

Also, couldn't there be multiple instances of Semigroup for Promise?

  1. Ignore failure (current implementation)
  2. First failure (see Brian's code)
  3. Concatenation of failures, like Validation (Really not sure if this analogy makes any sense)
mudge commented 11 years ago

Ah, thanks @puffnfresh. This should be relatively easy to do with something like:

/* concat :: Promise a -> Promise a */
Promise.prototype.concat = function (other) {
    var promise = new Promise();

    this.map(function (x) {
        other.map(function (y) {
            promise.resolve(x.concat(y));
        });
    });

    this.onRejected(function (reason) {
        promise.reject(reason);
    });

    other.onRejected(function (reason) {
        promise.reject(reason);
    });

    return promise;
};
puffnfresh commented 11 years ago

@ehd yes, wrappers to get different semigroups would be great.

mudge commented 11 years ago

It should be easy to provide a wrapper for both first-rejection and concatenated-rejection but the age-old question is what should the alternate functions be called? Is there a standard already?

seidtgeist commented 11 years ago

I'm not aware of a standard. Would we have to implement new methods, or would we implement a new type with different method implementations?

I'd vote for changing the current concat implementation to the first-rejection variant, like your code snippet above.

mudge commented 11 years ago

I've just pushed pacta 0.2.0 which should have this first-rejection behaviour in it. Please give it a whirl and let me know if that helps.