promises-aplus / promises-spec

An open standard for sound, interoperable JavaScript promises—by implementers, for implementers.
https://promisesaplus.com/
Creative Commons Zero v1.0 Universal
1.84k stars 164 forks source link

Only un-wrap one layer in `[[Resolve]]` #101

Closed ForbesLindesay closed 11 years ago

ForbesLindesay commented 11 years ago

I'd like to propose we re-visit the discussion surrounding the recursive nature of the new resolution algorithm. I understand that this is in-line with how many promise libraries currently operate, but I think we should consider whether it has to be that way as I have now managed to find some fairly compelling use cases of promises for promises and it does offer great gains in compatibility with monad systems.

I realize this is a contentious issue and these threads can quickly get very long and full of circular arguments. To help with this I have created 2 gists:

I remain personally fairly on the fence over this and have attempted to make both arguments as strong as I can.

For the first 36 Hours (until 2013-04-17T15:00:00Z) I would like people to refrain from commenting in this issue unless said comments are completely neutral. I would also like you to refrain from posting any rebuttals in either gist. Please read both gists, and add any arguments in favour of promises for promises to the For gist as comments and any arguments against promises for promises to the Against gist as comments.

I will attempt to merge any arguments I see in the comments into the main document, and will then delete those comments. The aim is to end that 36 hour period with two arguments that are both concise enough for everyone to read more or less in full and as solid/persuasive as they can be.

I would also like to request that everyone reads (or re-reads if you read them a while ago and need to refresh your memory) both of the following 2 documents:

domenic commented 11 years ago

First, we need to be much more precise with our usage of "promise" versus "thenable."

I think it's important to note that the spec does not prohibit promises for thenables. An implementation is allowed to create such things, and operate with them, and even hand them out to others. However, the spec does specify that if a non-promise thenable is assimilated, i.e. an unrecognized thenable from another implementation, it must be flattened. This behavior is meant to preserve boundaries between implementations. That's all!

Thus, allowing promises-for-thenables is an implementation choice left up to individual libraries. However, when libraries interact, such objects must not be forced upon an implementation that does support or allow such objects, i.e. that latter implementation must be protected from the design choices of a thenable-for-thenable producer.

(I had another paragraph here, but am moving it over to the gist in an attempt to respect the rules, since it ended up sounding more like advocacy than clarification.)

myrne commented 11 years ago

So the "argument collection" period seems to be over, and as I have said earlier (which I probably shouldn't have b/c of neutrality - for which I apologize) I feel for both standpoints. That's why I think it may be be counterproductive to turn this into a shoot-out of "are we going to have promises for promises - or not?". I think we would do best to first establish that we can't have both, or if we can't have both requirements in the way that that they are stated now, if we can't have the benefits of both with some creative solution.

I already suggested for the "promises for promises" proponents to simply "cede ground" (or stop attempting to take the current castle, depending on how you view the current state of the spec), and instead attempt something that leaves the "no promises for promises" proponents satisfied.

Most notably for me is that the people against "promises for promises" want something simple, while the proponents want something suitable for advanced use cases. Plus all or most have a FP background which I think could mean that calling the then function could well end up being "encapsulated" in a function which responsibility it is to call the then method.

As illustration, I want to present this piece of evidence:

function liftA2(pa, pb, f) {
    return ap(pb, map(pa, function(a) {
        return function(b) {
            return f(a, b);
        };
    }));
}

This is basically code from @pufuwozu attempted to make more readable in CoffeeScript and with slightly more descriptive variable names. This is taken from a code exampel by @pufuwozu .

The point is that @pufuwozu and others might see ways (beyond my imagination) to make great use of promises/nomads without ever having to see a single then (once a few "primitives" like map and ap have been developed). This suggests to me that for them, it may not matter that much in practice if the interface to the functionality now provided by then is by any means simple. It may very well be extremely cumbersome. But it gets hidden away, so it doesn't matter.

These are some ideas. 1 and 2 I mentioned earlier.

  1. We could make up a new method later, that unlike then only unwraps one layer. In practice, that would mean that the unwrapping would need to happen when the then method is called, instead of right after any onResolved or onRejected callback has returned a value. I think this would naturally be complemented with some kind of "flatten" or "unwrap" method, so that then would be equivalent to unwrap().later().

    I need to admit right away that I do not know exactly if this is technically possible. I've never worked on a Promise library (whatever the flavor). Intuitively, I think it can.

  2. To address the problem of potentially misbehaving promises, we could decide to require a library to not unwrap all promises and thenables it comes across. The spec could require a library to special-case its own promises (recognized by instanceof, or similar means). It could also require a library to special-case any promise which carries a "isPromisesACompliant" label.

    Now Domenic has already told me that at least some implementers (RSVP in this case) don't want to have promises for promises at all, and therefore don't bother to special case their own promises. This may not be the way to go.

  3. A third option would be is to introduce a dontTouchMyThen label, which implementers would respect, not out of agreement per se, but simply out of courtesy.

    I'd like to think of it as a minimalistic "robots.txt" for promises. I.e. the Web was designed with the idea that GET was safe, and therefore web crawlers could arise. The idea of a crawler is unthinkable without having a pre-agreed on safe method. However as it turned out, not everyone who'd like his pages indexed. Two ways where invented to address this problem: <meta name="robots" contents="noindex,nofollow"> and robots.txt. I actually do not know which came first, but that's largely beside the point. They were both means to address long-established and normally very desirable behavior of web crawlers, by allowing a way to change their default behavior.

  4. Lastly, also inspired by web technology, is the idea of sending along a "caller-identifier" (i.e. the User-Agent for promises). As web servers are able to change what they return on basis of user-agent sniffing, so could thenables. Thenables could turn some calls to their then into (effective) noops, depending on the "caller-indentifier", which would be the third argument to then. Perhaps better than a "caller-indentifier" would be a "purpose-indentifier". The purpose-identifier would specificy if the value is actually needed or not. Looking at like this, the purpose-identifier could be a simple boolean. It wouldn't even hurt extensibility of the meaning of the third argument, if all it would do is lock-down values for exactly true and false. Later the third argument could become a string or an options object.

    Note that with "effective noops" I don't mean they're actually noops. Internally, they might need to do some magic. Keeping track if their then has been called earlier at some point or so. I don't know. It's hard to imagine for me at this point. But I see potential here.

What do you think?

myrne commented 11 years ago

My lack of experience with Promise implemantions may become awfully clear... I think turning then into an (effective) noop (as proposed in 4) would cause infinite recursion in the standard promise resolution algorithm. It probably needs to be combined with something equivalent to a dontTouchMyThen label to stop recursion.

ForbesLindesay commented 11 years ago

Right, I now want to open this up to the floor for comments. Please ensure that you re-read each document. Many of them have changed significantly. You don't need to read the comments unless you want additional clarification. The comments in FOR discuss what it means for a promise to be lazy. The comments in AGAINST are largely about how you can recognize a foreign promise. Neither discussion relates directly to the issue at hand.

I would now like to propose that all discussion take place in this issue.

ForbesLindesay commented 11 years ago

I want to begin with the clarification that the current spec does not prohibit promises for promises. It also doesn't prohibit the then method only unwrapping a single layer of promises that are recognised in some way. It's only thenables which must strictly be unwrapped recursively. The specification also leaves how you recognise true promises completely open. As such one option would be a duck typing/branding test. Promises that want to only be unwrapped one layer could provide a .supportsPromisesForPromises label and promise libraries that want to support this could choose only to unwrap one layer for such promises.

The advantage of this is that such a specification could be developed independently of any existing specifications and could then prove its usefulness through use in real applications.

ForbesLindesay commented 11 years ago

It would also mean that a library that didn't want to allow promises for promises could still recursively flatten such thenables/promises at the boundary.

myrne commented 11 years ago

@ForbesLindesay Regardless of the spec, doesn't it also matter what the de-facto behavior is today? I mean, if the most prominent Promise libraries now all voluntarily choose to unwrap all layers, then that will be the expected behavior of consumers of said libraries. I.e. a consumer would expect that if it call "then" on some promise, the callback it supplies will get called with a non-promise value in return. Leaving this part of the spec open could be harmful then. Note, I'm not sure about the harmfulness. I've yet to gain experience with doing such advanced work on promises. Right now the only promises I've worked with were the promises I've created myself (by means of RSVP, that is).

myrne commented 11 years ago

Perhaps it calls for two excplicit "variants", "additions" to the spec, because both of these can be regarded as something positive, but mutually exclude each other:

  1. Guarantees unwrapping of all promises and thenables.
  2. Supports promises for promises.

I think it would be a good thing if a programmer could see up front what kind of library he's getting: Variant 1 or variant 2. Then both could benefit from an explicit mark. Solely developing a distinct "mark" for (2) leaves ambiguity in the behavior for the other libraries. I.e. do they provide any guarantees on unwrapping all promises and thenables?

Put differently, I think RSVP currently does more than it is asked for and - based on Domenic comments - this "more" is seen as a good thing. Better be explicit about it then. Simplicity is a selling point.

(I'd personally opt for a library which respects the .supportsPromisesForPromises property.)

myrne commented 11 years ago

Actually the library I'd choose would depend on what I'd be using it for. If I'd be using the library for "scripting", I'd choose a library that guarantees unwrapping, so that if I call "then" I get a value that I can use straight away. If I'd be developing a library based on promises on my own (say promised-builtins) then I'd choose for the non-unwrapping variant, so I can make use of laziness inside.

Actually, I think when coded right, a library of variety 2 could be easily made to adapt behavior of variant 1. All it needs is the then function on the promise prototype to be adjusted. This could be made as simple as requiring a single call after someone has loaded the lib. I.e. SomePromiseLib.autoUnwrapAllPromises().

juandopazo commented 11 years ago

attempted to make more readable in CoffeeScript

@meryn please don't. This is a JavaScript specification, let's keep the discussion in the same language.

@ForbesLindesay said:

I want to begin with the clarification that the current spec does not prohibit promises for promises. It also doesn't prohibit the then method only unwrapping a single layer of promises that are recognised in some way. It's only thenables which must strictly be unwrapped recursively.

This may seem that way, but in practice the story is very different. The lack of branding (which we discussed a while ago) left libraries with three options:

  1. Recognize everything with typeof obj.then == 'function' as a promise
  2. Recognize only its own promise implementation as valid and the rest as thenables
  3. Keep a list of valid implementations

(3) is simply impractical and many times impossible. Q does (2). YUI, when and most notably DOMFuture do (1). In fact, take a look at the text in DOMFuture:

If JavaScript IsCallable(then) is true, run these substeps then terminate these steps:

Let resolve be a future callback for the context object and its resolve algorithm.

Let reject be a future callback for the context object and its reject algorithm.

Call the JavaScript [[Call]] internal method of then with this value value and resolve and reject as arguments.

Emphasis mine. If I understood correctly, this effectively means that whatever thenable or promise will be flattened.

So we have two options going forward:

domenic commented 11 years ago

@meryn I'm sorry, but you've thrown such amazingly large walls of text up that I can't really keep up. If you want to summarize your points to like 200 words maybe we could all comprehend them better.

@juandopazo DOMFuture will almost certainly not change, and if await comes it will likely use duck-typing, based on discussions I've had. Q will also likely converge on (1). Finally, it's impossible for an object to change what Object.prototype.toString.call(obj) returns, so that particular branding mechanism isn't really feasible.

But in practice vs. in theory doesn't really matter. The point is, many libraries in practice don't want to allow promises-for-promises. That includes, apparently, YUI, DOMFuture, WinJS, and when. (Q will join them soon, most likely, and DOMFuture has an inconsistency that it allows creation of promises-for-promises via accept, sigh.) But if people are really excited about promises-for-promises, they can create a library that allows them! They just have to stay inside the boundaries of one library while they play with that stuff.

Maybe let's approach this the other way: what possible benefit would you derive from a JQPFAQPFAWJPFADFFAN? If each of those implementations recognized the others as conformant, then you could create such an entity. Why is that a good thing? What does it help you with?

myrne commented 11 years ago

@meryn please don't. This is a JavaScript specification, let's keep the discussion in the same language.

@juandopazo I replaced it with original code from @pufuwozu . Works too. :)

erights commented 11 years ago

@domenic writes

Q will also likely converge on (1).

I am very distressed to hear this. Likely it will mean that we will have to fork Q, as I don't see how my security goals can be met within (1). If this is not the right forum for this issue, please post a link to the right one.

domenic commented 11 years ago

@erights perhaps I misspoke; I meant only from the perspective of the [[Resolve]] algorithm, not more generally. Q(thenable) will still ensure that the result is a Q promise. If you're still concerned, let's definitely work together and not necessitate a fork (probably outside this issue, perhaps by email).

myrne commented 11 years ago

@domenic Read the first sentence of my bullet points. That will do as a summary I think. The summary of the summary is that I want to look for options to keep then mostly the same, but at the same time devise ways to accomodate for "promises for promises".

Another option (5), I just came up with: By default, let then unwrap all layers, but if passed a third argument (beyond onResolved and onRejected) - call it dontUnwrap - set to true, it won't unwrap all layers. Just like my suggestion for a new later method on promises (idea 1 in my previous post), this would require implementors to run the "promise-resolution" procedure at the time then is called, not directly after the onResolved callback has returned. I hope this is technically possible. But I've never implemented a promise-lib!

In general, I feel that the non-simple variant (the unwrapping behavior for then is certainly simple.. or should I say easy?) may be a little ugly, because it will often be hidden away behind abstractions (why I mentioned the liftA2 example).

domenic commented 11 years ago

@meryn what value do you derive from a JQPFAQPFAWJPFADFFAN? What problems does it help you solve?

erights commented 11 years ago

@domenic Ok, that wouldn't break security as far as I can see. But it would still break both promise pipelining (section 8 of http://www.erights.org/talks/promises/paper/tgc05.pdf . Nice summary at http://en.wikipedia.org/wiki/Futures_and_promises#Promise_pipelining ) and laziness. Please look again at the logic of Q.makeRemote in makeQ.js.

myrne commented 11 years ago

@domenic I'm not arguing for a JQPFAQPFAWJPFADFFAN :) I'm arguing for support for both recursive and non-recursive unwrapping. The possibility of getting a JQPFAQPFAWJPFADFFAN may be an accidental consequence. This is something the people using the non-recursive unwrapping "mode" must deal with. It shouldn't be your worry. :) I personally will make sure I'll keep sanity in any "layered" promises. But "lazy promises" sure would be nice! And aside from that, I'm of course curious what the Monad-guys will come up with. This wasn't my proposal. I'm just clinging on to something that looks nice to me. :)

erights commented 11 years ago

@meryn writes

But "lazy promises" sure would be nice!

No conflict here, as demonstrated by the existence proof of Q.makeRemote in makeQ.js

erights commented 11 years ago

@meryn No conflict that is, as long as a promise library does not unwrap its own promises, which is the issue just raised about Q.

myrne commented 11 years ago

@erights I need to study Q.makeRemote .

My reference point is an idea of mine for making "Promised Builtins". Basically proxies for builtins that behave just like their real counterparts. I think it would be great if I could pass them along without wondering if some are lazy, and some are really already in the process of being resolved (i.e. the async calls have been fired off). And if make a call to then, I'm sure to get the real, fully resolved value in either case.

I mentioned requiring implemenators not to unwrap their own promises in (2) of my earlier post. However, I do think that would result in a more complex API from the consumer point of view. The outward simplicity is the thing that @domenic likes a a lot, and I do too.

myrne commented 11 years ago

@erights where do I find "makeQ.js"?

myrne commented 11 years ago

Also, re JQPFAQPFAWJPFADFFAN: I think that with the proposed rule to only unwrap one layer, compliant libraries can never be the cause of this. Just replacing one foreign "promise layer" with a native promise-layer means the number of layers will stay equal. It won't decrease though. Any existing layering will remain. But this happens to protect the poor little lazy promise who doesn't want its then called until its value is absolutely required.

I think the most typical case will be that most of the time, most promises will be single layered (i.e. they would directly resolve to a value) and some will be double-layered promises of promises. The outward layer would effectively protect the inner layer from being touched. I don't see more than two layers happening in practice. At least, it's beyond my imagination of usefulness.

erights commented 11 years ago

@meryn https://code.google.com/p/google-caja/source/browse/trunk/src/com/google/caja/ses/makeQ.js and https://code.google.com/p/es-lab/source/browse/trunk/src/ses/makeQ.js .

juandopazo commented 11 years ago

@domenic

Finally, it's impossible for an object to change what Object.prototype.toString.call(obj) returns

(facepalm) Sorry, I meant promise.toString() === '[object Future]'. I agree that not trying to discern between promises and thenables is the way to go.

Why is that a good thing? What does it help you with?

You'd have to navigate the 6000 comments of the last few days to answer that. If I can put it in one sentence it would be: "doesn't break the monad laws and helps by introspection to discover opportunities for performance improvements".

bergus commented 11 years ago

@meryn:

By default, let then unwrap all layers, but if passed a third argument - call it dontUnwrap - set to true, it won't unwrap all layers

Why not the other way round? I'd be fine with that.

I mean, of the many people who have argued against promises for promises and like the synchronous-code metapher, who of you has ever encountered a promise/thenable for a thenable? I'd assume they just don't exist in "your world", so you don't have to worry about obtaining one. And should you really (have to) work with a lib that (maybe) uses/supports this crazy feature ("has that bug"), then just set a "and-please-flatten-everything-for-me" flag when assimilating one of these promises.

Those of us who are (planning to) use promises for promises in everyday code certainly don't want to have to care about always filling out that third parameter.

domenic commented 11 years ago

I'd assume they just don't exist in "your world"

They don't exist in our world because we flatten them. If we didn't, they'd be everywhere, as anyone who's tried to use jQuery and Q or when and Mongoose or WinJS and RSVP together can attest!

myrne commented 11 years ago

@domenic Could you explain the typical process of how these layered promises come into existence?

Would they still come into existence if every time a foreign promise was encountered, only the (foreign) outer layer gets replaced by a native layer? Because that's what up for discussion I think. (the outer layer could very well be the only layer, but that's beside the point).

ForbesLindesay commented 11 years ago

@meryn They wouldn't come into existence if all promise libraries worked like this and supported unwrapping one layer with each call to then. The reality is that many implementations don't unwrap at all. A chain of two sequential http requests could easily cause this problem if made using one of the non-conformant libraries. The current system protects you from the non-conformant libraries.

ForbesLindesay commented 11 years ago

@juandopazo

This may seem that way, but in practice the story is very different. The lack of branding (which we discussed a while ago) left libraries with three options

I think maybe I wasn't clear here. I wasn't suggesting we use branding to say "I'm a promise so you should unwrap me" since there are loads of promise objects which lack such branding. I was suggesting we brand this new variety of promises that supports/requires nesting. This seems fine because there aren't many such libraries at the moment so it's not too late for them to all agree on something. It should also be easier because (in order for it to be useful) it seems to be necessary for them to support other operations such as of, chain and empty

domenic commented 11 years ago

If other paradigms aside from promises, such as those using of/chain/empty, need this capability in those operations, then IMO they should include information about that in their specs for those methods. Such considerations should not impact the behavior of then.

juandopazo commented 11 years ago

@ForbesLindesay no, sorry. You were clear. I was just trying to make a point.

ForbesLindesay commented 11 years ago

@domenic that seems like a sensible idea to me.

For anyone who's interested, a better way of building the Request API might be to have a separate body property be a promise and resolve that to get the content.

myrne commented 11 years ago

The current system protects you from the non-conformant libraries.

@ForbesLindesay I think recognizing comformant libraries with a isPromiseAPlusCompliant property would suffice then.

Promises that want to only be unwrapped one layer could provide a .supportsPromisesForPromises label and promise libraries that want to support this could choose only to unwrap one layer for such promises.

While it of course doesn't matter for an implementation what exact "string" is used to set the property, I don't think supportsPromisesForPromises is the appropriate label. After all, the reason the recursive unwrapping is in place is to protect from libraries that don't bother to unwrap the promise they get before creating a new one. So that behavior is the behavior that should be declared explicitly. The label in effect would be saying: "I have not made the world any worse, you don't have to clean up after me."

I don't say my variant is right. Maybe coupling to the Promise/A+ "brand" is undesirable. I think then we should go for something like notUnnecessaryNested. Do you get what I'm saying?

domenic commented 11 years ago

@meryn I think you're still missing the point. We want to allow implementations to not do promises-for-promises. We can't specify some kind of magic brand which then allows those protections to be bypassed. In your phrasing, "I have not made the world any worse" is not something all libraries agree on. In my view, any library that produces promises for promises has made the world worse, and I need to put a stop to that craziness before it crosses into my own library.

domenic commented 11 years ago

Again, I'm a bit surprised this is so hard. As I emphasized originally, you can do promises for promises as much as you want. You just need to stay within your own library. Things get normalized when crossing library boundaries, not before.

myrne commented 11 years ago

We want to allow implementations to not do promises-for-promises.

Is this established? Is there a "we" who wants to allow implementations NOT to do promises-for-promises? And what is meant by "do"? Does it mean that there's a "we" that wants to allow implementations not to respect promises for promises? I thought it was all up for debate.

You're entitled to your opinion of course, as are many others who might agree with you. If I'm way off, then I'll think I'll just refrain from commenting further because I might be just adding noise.

ForbesLindesay commented 11 years ago

@meryn You're still missing the point as @domenic says. We aren't going to punch a magic hole through the existing implementations. I think what we can go for is go for a solution where all the libraries that support promises for promises can work together, so the idea can be experimented with properly.

ForbesLindesay commented 11 years ago

Those essages crossed in the post slightly. We are discussing that option, but I think it's highly unlikely to succeed in that form. A great many libraries explicitly don't want to support promises for promises.

myrne commented 11 years ago

@ForbesLindesay Thanks for pointing that out to me. I think I'll indeed keep quiet from now on. I hope that I've at least have added some value to the discussion. Otherwise, sorry for the noise. Let me know if there's a new spec up for debate. :)

puffnfresh commented 11 years ago

I would not like Promise libraries to provide an of method if they do not support promises of promises. My only request. Thanks.

juandopazo commented 11 years ago

Just to clarify the consensus part, at least I am not sure if flattening is the best approach. I haven't explored the problem space enough. While on one hand I appreciate the arguments behind flattening, I can't shake the feeling that dismissing 30 years of research in monads is a pity.

bergus commented 11 years ago

@domenic:

If we didn't flatten them, they'd be everywhere, as anyone who's tried to use jQuery and Q or when and Mongoose or WinJS and RSVP together can attest!

Flattening the one layer should prevent that, shouldn't it? Can you make a (code) example of how using these libs together results in an accidentally nested promise (if they didn't do recursion)?

@ForbesLindesay:

They wouldn't come into existence if all promise libraries worked like this and supported unwrapping one layer with each call to then.

That's what I was suspecting, and what I think we should specify if it's gonna work. And providing an alternative way for assimilating non-conformant thenables, instead of needing to assume that every thenable is non-conformant.

The reality is that many implementations don't unwrap at all.

You mean they implement then as a pure map? Uh. However, could you show me any examples of such libraries, and code in them using their non-conformant then so that nested promises come out? I mean, I would expect everybody to assimilate those promises things into conforming promises as soon as possible, and not using their then method at all.

bergus commented 11 years ago

@pufuwozu wrote:

I would not like Promise libraries to provide an of method if they do not support promises of promises.

Hey, that could be a clever idea to detect conforming promises/thenables! I'd think of a [[resolve]] procedure such as

if (value.then is a function) // thenables, including promises
    if (value.constructor.of is a function) // it's a monadic promise!
        unwrap exactly one layer
    else
        assimilate by recursively running [[resolve]]
domenic commented 11 years ago

Can you make a (code) example of how using these libs together results in an accidentally nested promise (if they didn't do recursion)?

var nestedPromise = $.when(Q(5));

var anotherOne = $.when(5).then(function () { return Q(10); });

var realisticExample = $('.foo').animate('opacity', 0).promise().then(function () {
    return getDataFromServerUsingQ();
});

Three levels:

var _Deferred = require('underscore.deferred');

var threeLevels = _.when($.when(Q(5)));

Etc. as I pile on more and more libraries.

bergus commented 11 years ago

@domenic:

  _.when($.when(Q(5)));

OK, but nesting constructors is something you wouldn't do, would you? Actually I'd say something like that (maybe with of) would be the preferred way to explicitly nest promises :-) However, this would be resolvers-spec, not what we're discussing here…

Your anotherOne or realisticExample is much more interesting. So jQuery doesn't unwrap Q promises but treats them as values? Fine (not that it's unexpected). But how would go about fixing that? I only can think of either using

ConformantPromise.assimilate($('.foo').animate('opacity', 0).promise())
  .then(getDataFromServerUsingQ)
  .then(…);

or

ConformantPromise.recursiveAssimilate(realisticExample).then(…);

as just going on with jQuery's realisticExample.then(…) would not work. So we have to fix it explicitly anyway, don't we?

domenic commented 11 years ago

@bergus yes, you would need to do a transformation into a realistic example for the three-level case; I left that as an exercise to the reader.

Let's try again. This will also address your other question.

var takeThree = getDataFromServerUsingQ().then(function (data) {
    return $('.foo').animate('opacity', data.opacityLevel).promise().then(function () {
        return updateBackboneModelViaSomeThirdPartyLibraryUsingUnderscoreDeferred().then(function () {
            return tellServerThatTheUpdateSucceededUsingQ();
        });
    });
});

If Q, as a proper Promises/A+ library, does recursive [[Resolve]], this is a promise for undefined that will be rejected with the appropriate error if any of the operations failed. But if it did one-level unwrapping, this would be a QPFAUDPFAQPFU, and it would always fulfill---failure information would have to be manually extracted.

medikoo commented 11 years ago

I use promises a lot, worked out a lot of complex async flows with them, and I understand this discussion as: Reconsider whether promise is a promise, maybe it isn't ;-)

It's a dispute over a fact whether a promise is a real value (type that we treat same on same level as we treat an array, string, number etc.) or not (as it's treated now by flattening it to resolved value whenever possible).

I take that if we treat it on pair with other types, then we're actually no longer talking about promises but about some other custom type, that serves uncertain purpose. It will no longer be a promise as it will no longer be aid for asynchronicity (as it is now with currently accepted characteristics).

I'll add to @domenic example, copy of my example from Promise for Promises gist, of how with current common usage, lazy promises (not flattened) will behave:

var lintDirectory = function (path) {
  return readdir(path).map(function (fileName) {
     return readFile(fileName).then(lint);
  });
};

// No lazy promises (as current implementations do)
lintDirectory(path).then(function (reports) {
  console.log(reports); // [...] array of lint reports
});

// Lazy promises
lintDirectory(path).then(function (readdirPromise) {
  readdirPromise.then(function (readFileMapPromise) {
    readFileMapPromise.then(function (lintMapPromise) {
      lintMapPromise.then(function (reports) {
        console.log(reports); // [...] array of lint reports
      });
    });
  })
});
domenic commented 11 years ago

Indeed, I like the way @medikoo phrases it. There's---practically speaking---nothing wrong with being a monad on the category of non-thenables.

erights commented 11 years ago

There's---practically speaking---nothing wrong with being a monad on the category of non-thenables.

Excellent!