Re: Futures

On Fri, Apr 26, 2013 at 11:44 AM, Juan Ignacio Dopazo
<dopazo.juan@gmail.com> wrote:
> 2013/4/26 Tab Atkins Jr. <jackalmage@gmail.com>
>>
>> On Fri, Apr 26, 2013 at 11:25 AM, Kevin Smith <zenparsing@gmail.com>
>> wrote:
>> > Actually, I may have gotten it terribly wrong (apologies).  In my
>> > prototype
>> > implementation, the following:
>> >
>> >     Future.accept(Future.resolve(1)).then(value => {
>> >
>> >         console.log(value !== 1);
>> >         return Future.accept(Future.resolve(1));
>> >
>> >     }).then(value => {
>> >
>> >         console.log(value === 1);
>> >     });
>> >
>> > logs
>> >
>> > - true
>> > - true
>> >
>> > Is that what it should be doing, according to the DOM spec?  Anne, Alex?
>>
>> No, it should be "true", then "false".
>>
> I think Kevin's assertion is correct. According to the spec, callbacks are
> wrapped in something called a "future wrapper callback". When a promise is
> returned from the callback, the wrapper does this:
>
>> Let value be the result of invoking callback with argument as argument.
>> (...) run resolver's resolve with value and the synchronous flag set.
>
>
> resolve tries to adopt the promise by being recursive, effectively
> flattening the promise:
>>
>> If value is a JavaScript Object, set then to the result of calling the
>> JavaScript [[Get]] internal method of value with property name then.
>> If JavaScript IsCallable(then) is true [treats all thenables the same
>> way], run these substeps and then terminate these steps:
>> Call the JavaScript [[Call]] internal method of then with this value value
>> and resolve and reject as arguments.
>
> If resolved called the thenable's then() with accept and reject, it would
> only unwrap one layer.

Gah, you're right.

So, this is a problem.  The current behavior is incoherent wrt types,
and is *inconsistent* between its first and second callbacks.

First, incoherency.

Given a Future<a>, its .then() method accepts two callbacks, each of
which is supposed to have the signature "a -> Future<b>".  We apply
some magic to this so that you can also use functions of type "a -> b"
(where "b" is not a Future) and we auto-massage it into working
correctly.  So far so good - this lets us chain Futures, which is
great.

Now, if you omit one of the callbacks, we need to supply a "default"
behavior.  This needs to match the signatures above.  In the Futures
spec, both Future.accept() and Future.reject() have the signatures "a
-> Future<a>", which is exactly what we need.  Future.resolve(), on
the other hand, has the signature "(a or Future<a>) -> Future<a>",
which is inconsistent with the above.  Signatures like this can be
*useful*, so that calling sites can pass in both plain values and
Futures, but it doesn't match the required signature for the .then()
callbacks, and so it's unacceptable for a default behavior.

Second, inconsistency.

As I explained above, the signature of Future.reject() is "a ->
Future<a>".  The signature of Future.resolve() is "(a or Future<a>) ->
Future<a>".  The fact that these two signatures are different means
that omitting a callback will have *observably different behavior*
based on which one you omitted.

For example, in the case of
"Future.accept(Future.accept(5)).then().then(alert)", it will alert
"5", because the resolve semantics for the missing callback in the
first .then() will perform extra unwrapping of the value.  (In other
words, it gives you a *different result* than just doing
"Future.accept(Future.accept(5)).then(alert)", which will alert
"<object Future>" or whatever.)

On the other hand, in the case of
"Future.reject(Future.reject(5)).then().then(null, alert), it will
alert "<object Future>".  (And you'll get the same behavior if you
remove the intermediary ".then()".)

This sort of inconsistency is *bad news* for authors trying to use
Futures, as it means that the exact path the value took through
accept/reject callbacks will change the value that it gets.

~TJ

Received on Friday, 26 April 2013 20:31:36 UTC