couchdeveloper / RXPromise

An Objective-C Class which implements the Promises/A+ specification.
Other
276 stars 29 forks source link

Interest in aligning with ES6 Promise semantics? #21

Open stefanpenner opened 10 years ago

stefanpenner commented 10 years ago

I understand this is objective-c land, but as promises in JavaScript are now part of a formal standard, is there interest in aligning this project with those semantics?

One very specific and confusing difference is how the promises compose.

in ES6: there is no concept of a promise fulfilling with another promise, when a promise is to resolve another promise, it merely assimilates the new promises state.

Simple example:

var rejectedPromise = Promise.reject(new Error);
var promise = Promise.resolve(rejectedPromise)

promise.then(function(){
  // will not invoke
}).catch(reason) {
  // will invoke, with the reason of the assimilated promise
  // to propagate the rejection
  throw reason;

  // return to "catch" and handle the rejection, providing a new success value"
  return 1;

  // return a promise, to "retry"
  return User.find(1);
}); 

Why is this useful?

It encourages encapsulation, and mimics synchronous programmings try/catch

example continued:

sync:

function fetch() {
  var something = ajax('/something');
  var somethingElse = ajax('/something-else/' + something.id);
  // if either attempt throws, fetch throws
  return something;
}

async


function fetch() {
  return ajax('/something').then(function(value) {
    return ajax('/something-else/' + value.id);
  });
}
// if either attempt rejects, the promise returned from fetch rejects

// refactor further
function fetchSomethingElse(value) {
  return ajax('/something-else/' + value.id);
}

function fetch() {
  return ajax('/something').then(fetchSomthingElse);
}
// identical semantics.

To summarize, these chaining and assimilation semantics encourage best practices. Although this library is clearly not JavaScript, aligning only improves mindshare on the topic.

couchdeveloper commented 10 years ago

I understand this is objective-c land, but as promises in JavaScript are now part of a formal standard, is there interest in aligning this project with those semantics?

RXPromise tries to follow the Promises/A+ spec. Especially, regarding your question, it tries to accomplish this:

"If x (the value which is used to revolve the promise) is a thenable, it attempts to make promise (the receiver) adopt the state of x, under the assumption that x behaves at least somewhat like a promise."

In other words, in RXPromise, if a pending promise B will be resolved with another promise A (either pending or resolved), the promise B "adopts the state" of promise A. More precisely, the set of continuations registered for A and B will execute on their respective execution context when the promise A gets resolved, and both promises now have the same state (either fulfilled or rejected - and in RXPromise possibly cancelled).

Cancellation will be handled as well in a similar fashion. If promise B is already cancelled when the implementation adopts the state, promise A will be cancelled as well. Likewise, when the cancellation on promise B happens after the adoption, promise B also gets cancelled and vice versa.

If I take your example:

RXPromise* something = [[RXPromise alloc] initWithResult:original_error];  // state is rejected
RXPromise* promise = [[RXPromise alloc] init];
[promise resolveWithResult:something];   // note: will invoke synced_bind:

promise.then(nil, ^id(NSError* error) {
    // Parameter `error` is the value of the assimilated promise `something`:
    assert(original_error == error);

    Then you have the following options:
    // return a value (maybe be nil) in order to proceed normally:
    return @"OK";   // or: return nil;

    OR: 
    // "rethrow" the error:
    return error;

    OR: 
    // "throw" another error:
    NSError* newError = [NSError errorWithDomain:  ...];
    return newError;
});
stefanpenner commented 10 years ago

I believe my confusion/concern boils done to:

https://github.com/couchdeveloper/RXPromise/blob/6466cec5cae1f61a5c056ec360486320322915e0/RXPromise%20Libraries/Source/RXPromise.mm#L879-L896

  1. promise acting as a deferred (exposing its private resolve/reject)
  2. exposing both fulfill and resolve (I would like to suggest "resolve" be the only one of the two)

Everything else I have encountered so far, (as I get used to the Objective-c'isms) feels good, good job.

couchdeveloper commented 10 years ago

promise acting as a deferred (exposing its private resolve/reject)

Well, yes. Possibly, a solution would be to have a class RXDeferred which subclasses RXPromise. This would make the API more clean. However, since in this case a RXPromise IS_A RXDeferred actually, anyone can send "deferred" messages to a promise, and it would act accordingly as a deferred.

Another requirement is to let a resolver easily subclass a RXPromise (respectively a Deferred, it it were split). There are a few important use cases which require a subclass of RXPromise. So, splitting RXPromise into a Promise and a Deferred, should guarantee that this is still possible.

Not sure why I really chose this design, perhaps since Objective-C usually likes to use conventions over, well, compiler errors. So, here the convention for a consumer is, "don't resolve a promise" ;) A couple months ago, I implemented a C++ version of a promise where I chose a to make the deferred API private, very much as an effect of a knee-jerk reaction. So, I believe this design choice has something to do with the kind of language ;)

But seriously, I will investigate the options to make the deferred part a subclass and thus a "declared private" API for the consumer, unless you know a better solution. I would also prefer a "light weight" implementation relying on conventions over a "strict" implementation which could only be accomplished with a more complex class layout.

exposing both fulfill and resolve (I would like to suggest "resolve" be the only one of the two)

Strictly, if there is a resolveWithResult: method, it makes the API fulfillWithValue: and the API rejectWithReason: redundant. Nonetheless, I need the implementations of both. IFF there will be a change in the Promise/Deferred API, I may consider to make the API of the deferred more concise and crisp as you suggested.

Everything else I have encountered so far, (as I get used to the Objective-c'isms) feels good, good job.

Thanks :)

stefanpenner commented 10 years ago

[RXPromise promiseWithResult:anotherPromise]

should mimic the ES spec's Promise.resolve

It should accept a value, a promise, or an error. When accepting a promise as it's argument the newly constructed promise's fate should be that of the promise passed as the argument.

https://github.com/couchdeveloper/RXPromise/blob/6466cec5cae1f61a5c056ec360486320322915e0/RXPromise%20Libraries/Source/RXPromise.mm#L867

this is part of my first example:

var error = new Error();
var rejectedPromise = Promise.reject(error);
var promise = Promise.resolve(rejectedPromise)

promise.then(function(value) {
  // never invoked
}, function(reason) {
  reason === error // => true
});
stefanpenner commented 10 years ago

Strictly, if there is a resolveWithResult: method, it makes the API fulfillWithValue: and the API rejectWithReason: redundant. Nonetheless, I need the implementations of both. IFF there will be a change in the Promise/Deferred API, I may consider to make the API of the deferred more concise and crisp as you suggested.

Ya it would appear that in objective-c land we need only the following:

[RXPromise resolveWithResult:...] and [RXPromise promiseWithResult:...]

Where [RXPromise resolveWithResult:foo] is basically just

 if ([foo class] == self) {
   return foo
} else {
  return [self promiseWithResult:foo];
}

In theory though, wonder if we need both, resolveWithResult may be sufficient (in objective-c)