A Challenge Problem for Promise Designers
Le 26/04/2013 14:54, Kevin Smith a écrit :
What exactly is the controversy here?
I think we all agree with the semantics of "then" as specified in Promises/A+. (If not, then we have a really big problem!)
If so, then the only real controversy is whether or not the API allows one to create a promise whose eventual value is itself a promise. Q does not: it provides only "resolve" and "reject". DOM Futures do by way of "Future.accept". As far as I know, there's nothing about Q's implementation that would make such a function impossible, it just does not provide one.
I believe at this point the question isn't so much "can I build a promise for a promise?", but rather "what should be the default Future semantics?" Namely:
Future.accept(5)
.then(function(x){
return Future.accept(x);
})
.then(function(y){
// is y a Future?
})
I'm arguing in favor of y being guaranteed to be a non-Future value. It is my understanding others would want y to be a Future. That would be the controversy as I understand it.
On 26 April 2013 14:29, David Bruant <bruant.d at gmail.com> wrote:
Le 26/04/2013 13:24, Andreas Rossberg a écrit :
I'm not sure if your description of E is accurate -- I'd find that surprising. It is a perfectly sensible design to have transparent futures that you can just use in place of the value they eventually get resolved to (let's call that value their 'content'). In fact, that is how futures/promises where originally proposed (back in the 70s) and implemented (e.g. in MultiLisp in 85, Oz in 95, and others later). However, there are really only two consistent points in the design space:
Either, futures are objects in their own right, with methods and everything. Then they should be fully compositional, and never be equated with their content, even after resolved (because that would change the meaning of a reference at locally unpredictable points in time).
Or, futures are entirely transparent, i.e. can be used everywhere as a placeholder for their content. In particular, that means that 'f.then' always denotes the 'then' method of the content, not of the future itself (even when the content value is not yet available).
Unfortunately, option (2) requires that accessing a future that is not yet resolved has to (implicitly) block until it is (which is what happens in MultiLisp and friends).
I'm adventuring myself in places where I don't have experience, but I dont think blocking is what has to happen. Programming languages I know all have a local-by-default semantics, that is all values being played with are expected to be local by default (which sort of makes sense for in single-machine environments). A programming language could take the opposite direction and consider all values as remote-by-default. If values are remote, with event-loop semantics, you never really block.
Note that futures/promises, by themselves, do not have anything to do with distribution. But yes, you could implicitly wrap every operation into a separate computation returning a future. That would be pretty close to what lazy languages do (a lazy thunk is basically a form of future). Unfortunately, it is also known to interact horribly with side effects, by making their order highly unpredictable (despite the semantics still being deterministic).
On Fri, Apr 26, 2013 at 9:36 AM, David Bruant <bruant.d at gmail.com> wrote:
Le 26/04/2013 14:54, Kevin Smith a écrit :
What exactly is the controversy here?
I think we all agree with the semantics of "then" as specified in Promises/A+. (If not, then we have a really big problem!)
If so, then the only real controversy is whether or not the API allows one to create a promise whose eventual value is itself a promise. Q does not: it provides only "resolve" and "reject". DOM Futures do by way of "Future.accept". As far as I know, there's nothing about Q's implementation that would make such a function impossible, it just does not provide one.
I believe at this point the question isn't so much "can I build a promise for a promise?", but rather "what should be the default Future semantics?" Namely:
Future.accept(5) .then(function(x){ return Future.accept(x); }) .then(function(y){ // is y a Future? })
I'm arguing in favor of y being guaranteed to be a non-Future value. It is my understanding others would want y to be a Future. That would be the controversy as I understand it.
Reframing this in terms of resolution semantics may be helpful. I think others would want a way to resolve one promise without recursively resolving them all. That doesn't mean that the default resolution semantics can't recursively resolve. Recursive resolve can be built on top of a single-step resolver, but even so, it would be useful to also provide (and recommend) recursive resolve resolve since it has the resolution semantics demanded by the vast majority of use cases.
The fundamental controversy, as Juan just noted, is how to precisely
identify a promise in order to do either of these two things. This problem
isn't quite so clean cut, but it's much more important to solve. I've been
trying to bring some attention to it over the last few days -- I hope it's
clear that a then
method is not enough to identify a promise language
construct -- this will subtly break existing code (e.g. casperjs).
In passing people keep assuming a well-known symbol could solve this
problem, but haven't offered any ideas for compatibility with es5 code. I
can't see how we could shim enough of the module system to use it as a
registry here, but maybe we could hang a symbol of a built-in Promise class
that could be shimmed as a random string. Given a built-in Promise class I
believe an instanceof
branding could be made to work in a shimmable
manner too, with proto hacking getting you the rest of the way there if
it really comes to it (es5 cross-frame wackiness, for instance).
On 26 April 2013 16:25, Dean Landolt <dean at deanlandolt.com> wrote:
The fundamental controversy, as Juan just noted, is how to precisely identify a promise in order to do either of these two things. This problem isn't quite so clean cut, but it's much more important to solve. I've been trying to bring some attention to it over the last few days -- I hope it's clear that a
then
method is not enough to identify a promise language construct -- this will subtly break existing code (e.g. casperjs).
Let me note that this is not the fundamental controversy (not for me, anyway). The fundamental controversy is whether there should be any irregularity at all, as is unavoidably introduced by implicit flattening. The problem you describe just makes the negative effect of that irregularity worse.
Le 26/04/2013 17:25, Tab Atkins Jr. a écrit :
On Fri, Apr 26, 2013 at 3:19 AM, David Bruant <bruant.d at gmail.com> wrote: Your abstract example was:
If Future<Future<x>> can exist, then you'll have to write this boilerplate code in a lot of places: f.then(function res(v){ if(Future.isFuture(v)){ v.then(res); } else{ // actual work with the resolved value. } }) I don't understand why this boilerplate code has to exist, or why it does anything useful for you. Fundamentally, your example seems to show you having to react to a badly-authored outside environment, where somebody fucked up and double-wrapped a value by accident.
No, please read the last part of [1]. A function can change of signature. That happens anytime a then callback used to return a non-promise value and suddenly returns a promise because the computation moved from local to remote. So changing from p.then(v =>{ return someLocalComputation()+v; })
to p.then(v => { return someRemoteComputation().then(x => x+v) })
Such refactoring happens and isn't an accident. If flattening doesn't happen, all consumers of the next promise have to add unwrapping boilerplate.
This is exactly like arguing that Array#map needs to fully flatten, in case people accidentally pass in [[1, 2, 3]].
Some APIs will flatten nested arrays. It depends on the use case. In that instance, no one has really provided concrete use cases where that would be useful for promises beyond writing a flattening abstraction or such. The best argument I've heard so far was from Andreas and it's solved if the low-level API is provided. It doesn't make it less useful to have flattening by default.
The main reason people have given for this potentially happening is authors mixing promise libraries, and the libraries not recognizing each other's promises
That's something I didn't talk about during the flattening discussion (until realizing both issues are actually connected) and chose to completely ignore as I actually am in disagreement with others on that point (though I'll probably have to resolve myself to accepting it by lack of better idea). My arguments are in the realm of software engineering, like ease of refactoring, code maintainability, readability.
If you get stacked promises on purpose
What are the use cases where you want that besides when writing a promise abstraction (like flattening)?
then obviously you don't want to break the feature by having .then() recursively unwrap.
I would love to see a concrete example of a nested promise problem that's not a result of an authoring error (which seems hard to do on its face - at least with Futures, it seems difficult to accidentally double-wrap)a weird non-JS environment (like Dean Tribble's example, which turned out to be invalid for JS promises and DOM Futures), or a mixing of major libraries with bespoke promises that don't mutually recognize each other (because this will go away reasonably fast now that we have standardized Futures).
Refactoring and not having to rewrite/add boilerplate all the promise consumer.
I would love to see a concrete example of a nested promise use case that isn't an abstraction that better belongs in a library.
David
Le 26/04/2013 20:36, Rick Waldron a écrit :
The libraries discussed in this and similar threads have the benefit of very limited adoption, where breaking changes incur minimal costs. jQuery doesn't have that luxury ;) [0] and therefore won't break backward compatibility. I can assure you that we won't press for adoption of our implementation as a standard—despite its more than adequate qualification as a de facto standard[1] (like it or not).
Which naturally leads to the question: why should platform promises be compatible with Promise/A+ and not jQuery "promises"? Because more libraries use Promise/A+? what about market share?
From: David Bruant [bruant.d at gmail.com]
Which naturally leads to the question: why should platform promises be compatible with Promise/A+ and not jQuery "promises"? Because more libraries use Promise/A+? what about market share?
What we're discussing is not compatibility but ability to assimilate. Assimilating thenables requires no particular spec compliance or library compatibility. Promises/A+ (in the 1.1 version) gives a step-by-step procedure for doing so that is quite resilient in the face of edge cases, and so I'd recommend it for any assimilation semantics, but that's not terribly relevant to the question of whether there should be assimilation semantics at all.
On Fri, Apr 26, 2013 at 3:47 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:
From: David Bruant [bruant.d at gmail.com]
Which naturally leads to the question: why should platform promises be compatible with Promise/A+ and not jQuery "promises"? Because more libraries use Promise/A+? what about market share?
What we're discussing is not compatibility but ability to assimilate.
The ability to assimilate stems directly from a need for library compatibility. Seriously -- ask Kris Kowal who twisted his arm into having Q accept thenables? :P
Assimilating thenables requires no particular spec compliance or library compatibility. Promises/A+ (in the 1.1 version) gives a step-by-step procedure for doing so that is quite resilient in the face of edge cases, and so I'd recommend it for any assimilation semantics, but that's not terribly relevant to the question of whether there should be assimilation semantics at all.
What I'd really like to know is what is supposed to happen when a casper.js [1] instance is returned by a promise? There is a lot of this code in the wild. It's one thing when we're just talking about libraries which users intentionally choose. But baking these assimilation semantics into the language will create subtle interactions that are non-trivial to find and debug. And for what? Compatibility with existing Promises/A+ libraries that could easily make themselves compatible in other ways? That hardly seems worth it.
[1] casperjs.org
Le 26/04/2013 21:47, Domenic Denicola a écrit :
From: David Bruant [bruant.d at gmail.com]
Which naturally leads to the question: why should platform promises be compatible with Promise/A+ and not jQuery "promises"? Because more libraries use Promise/A+? what about market share?
I realize I was playing the douchebag devil advocate here and I apologize for that.
The point I was trying to make is that there is no reason for the platform to follow a standard more than another. PromiseA+ has some user base and benefits, but jQuery has a much more massive user base and it would make sense for the platform to follow that. There are probably a bunch of other de facto standard here and there. Why should platform promises be more interoperable with promiseA+ and not any other (de facto) standard?
What we're discussing is not compatibility but ability to assimilate.
I confused both as "having some level of interoperability".
Assimilating thenables requires no particular spec compliance or library compatibility. Promises/A+ (in the 1.1 version) gives a step-by-step procedure for doing so that is quite resilient in the face of edge cases, and so I'd recommend it for any assimilation semantics, but that's not terribly relevant to the question of whether there should be assimilation semantics at all.
As is, the algorithm is unacceptable as it unconditionally calls the then method of a thenable potentially resulting in unintended side-effects if the thenable wasn't intended to be a promise but a non-promise value. I believe "does this object has a 'then' method?" is too weak of a heuristic for a web platform convention. Trying an idea: Step 2.3 of the Promise Resolution Procedure, instead of just "if then is a function", what about "if then is a function and has a property named 'IAmAPromiseA+Thenable' which value is true, call it... "?
I don't think it would be possible (you'll tell me soon enough I imagine), but that would be better.
Although this sounds crazier, it's a much better way to express the intent that such then function is a thenable and I wouldn't be opposed to this test even baked in the platform as it dramatically reduces the risk of confusion.
On Fri, Apr 26, 2013 at 6:36 AM, David Bruant <bruant.d at gmail.com> wrote:
Le 26/04/2013 14:54, Kevin Smith a écrit :
What exactly is the controversy here?
I think we all agree with the semantics of "then" as specified in Promises/A+. (If not, then we have a really big problem!)
If so, then the only real controversy is whether or not the API allows one to create a promise whose eventual value is itself a promise. Q does not: it provides only "resolve" and "reject". DOM Futures do by way of "Future.accept". As far as I know, there's nothing about Q's implementation that would make such a function impossible, it just does not provide one.
I believe at this point the question isn't so much "can I build a promise for a promise?", but rather "what should be the default Future semantics?" Namely:
Future.accept(5) .then(function(x){ return Future.accept(x); }) .then(function(y){ // is y a Future? })
I'm arguing in favor of y being guaranteed to be a non-Future value. It is my understanding others would want y to be a Future. That would be the controversy as I understand it.
No. Future callbacks can return Futures, which then chain (the return value of then adopts the state of the callback's return value). This is the big "monad" benefit that we keep talking about.
The only way to get y to be a future is to have the first function do "return Future.accept(Future.accept(x));". In other words, you have to be pretty explicit about this.
The fact that Future callbacks can return either plain values or Futures, and they act consistently with both (sending a plain value to the next callback in the chain) is extremely powerful and usable, and means that you basically never have to explicitly wrap a callback, or even worry about the return value of functions. As long as your callback is simply returning the function's return value, it can be a Future or a plain value, and it'll just work with you none the wiser.
If other people on the "recursive flattening" side have the same misunderstanding here that you're showing, no wonder we're all arguing with each other so fiercely! You're completely correct that having Y be a Future would be terrible, and that's precisely why Futures work the way they do already.
(For the uninitiated, the chaining behavior is part of the monad contract. If y was a Future, it would mean that Futures were only functors (a weaker abstraction than monads). Luckily, they're not.)
(By the way, apologies if any of this sounds insulting. It's unintentional! I can't figure out a way to phrase "You're wrong, and everyone else might be wrong in the same way" in an unambiguously polite manner. ^_^)
On Fri, Apr 26, 2013 at 1:39 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote: [snip]
No. Future callbacks can return Futures, which then chain (the return value of then adopts the state of the callback's return value). This is the big "monad" benefit that we keep talking about. [snip]
Shorter me: this is why I keep asking people who want flattening to actually provide an example of where flattening is useful, that isn't (a) assimilation, (b) a result of weird language semantics from some non-JS language, or (c) an authoring error.
So far, I haven't gotten one! Every concrete example I've received so far just shows off assimilation, or a misunderstanding of what JS promises/futures do. :/
From: Tab Atkins Jr. [jackalmage at gmail.com]
Shorter me: this is why I keep asking people who want flattening to actually provide an example of where flattening is useful, that isn't (a) assimilation, (b) a result of weird language semantics from some non-JS language, or (c) an authoring error.
Since (multi-level) flattening only occurs for assimilation (per Promises/A+ 1.1), it appears we have been talking past each other. All examples of multi-level flattening will necessarily be examples of assimilation.
On Fri, Apr 26, 2013 at 1:45 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
From: Tab Atkins Jr. [jackalmage at gmail.com]
Shorter me: this is why I keep asking people who want flattening to actually provide an example of where flattening is useful, that isn't (a) assimilation, (b) a result of weird language semantics from some non-JS language, or (c) an authoring error.
Since (multi-level) flattening only occurs for assimilation (per Promises/A+ 1.1), it appears we have been talking past each other. All examples of multi-level flattening will necessarily be examples of assimilation.
In that case, HUZZAH! We've solved the problem! Assimilation is not a troublesome issue; if that works best when done recursively, go for it. Presumably, the reason it works best when done recursively is that if the value passed between multiple non-assimilating promise concepts, it might have gotten multi-wrapped, once for each promise concept (or maybe more than once for each, if the layers are separated by other types of promises). So, it's more useful to just assume that multi-wrapped thenables were done accidentally, and should be fully flattened.
If that's all we need to do, and we can have an explicit assimilation function in the standard that takes arbitrary thenables (per the Promises/A+ spec), then we can leave .then() alone and give it the correct monadic semantics. It's very difficult to accidentally get double-wrapped within a single library with proper semantics, and doing so indicates a fundamental logic error and so should give funky results, rather than having the error accidentally papered over.
On Fri, Apr 26, 2013 at 1:39 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Fri, Apr 26, 2013 at 6:36 AM, David Bruant <bruant.d at gmail.com> wrote:
Le 26/04/2013 14:54, Kevin Smith a écrit :
What exactly is the controversy here?
I think we all agree with the semantics of "then" as specified in Promises/A+. (If not, then we have a really big problem!)
If so, then the only real controversy is whether or not the API allows one to create a promise whose eventual value is itself a promise. Q does not: it provides only "resolve" and "reject". DOM Futures do by way of "Future.accept". As far as I know, there's nothing about Q's implementation that would make such a function impossible, it just does not provide one.
I believe at this point the question isn't so much "can I build a promise for a promise?", but rather "what should be the default Future semantics?" Namely:
Future.accept(5) .then(function(x){ return Future.accept(x); }) .then(function(y){ // is y a Future? })
I'm arguing in favor of y being guaranteed to be a non-Future value. It is my understanding others would want y to be a Future. That would be the controversy as I understand it.
No. Future callbacks can return Futures, which then chain (the return value of then adopts the state of the callback's return value). This is the big "monad" benefit that we keep talking about.
To lay it out even more clearly for any bystanders, in the following code:
getAFuture() .then(function(x) { return doSomeWork(x); }) .then(function(y) { // is y a Future? });
The answer to the question is "no", regardless of whether doSomeWork returns a plain value or a Future for a plain value.
If doSomeWork() returns a plain value, the future returned by the first .then() call accepts with that value. If doSomeWork() returns a future that eventually accepts, the future returned by the first .then() call waits until it accepts, and then also accepts with the same value.
This all happens without recursive unwrapping. It's just a feature of how this kind of thing works (it's a result of Futures matching the "monad" abstraction).
The only way that y will become a Future is if doSomeWork() explicitly and purposefully returns a future for a future for a value (in other words, Future<Future<x>>). In this case, the future returned by the
first .then() call waits until the outer Future from the return value finishes, then accepts with its value, and "this value" happens to be a Future<x>.
This sort of thing does not happen accidentally. It's hard to make nested futures unless you're doing it on purpose, or you have no idea what you're doing. In the first case, we want to trust the author, and in the second case, it's probably better for everyone involved if the code fails in a fairly obvious way, rather than attempting to paper over the problem. If you're competent and just doing the natural thing, the API naturally gives you plain values in callback arguments and singly-wrapped futures in return values.
This is why I've been arguing against recursive unwrapping in the general case.
On Fri, Apr 26, 2013 at 1:51 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:
On Fri, Apr 26, 2013 at 1:45 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
From: Tab Atkins Jr. [jackalmage at gmail.com]
Shorter me: this is why I keep asking people who want flattening to actually provide an example of where flattening is useful, that isn't (a) assimilation, (b) a result of weird language semantics from some non-JS language, or (c) an authoring error.
Since (multi-level) flattening only occurs for assimilation (per Promises/A+ 1.1), it appears we have been talking past each other. All examples of multi-level flattening will necessarily be examples of assimilation.
In that case, HUZZAH! We've solved the problem!
We may have.
When I argue for default flattening (#0 in my "What Are We Arguing About") and you argue against, you claim default flattening is not monadic, which I agree with. But then you go on to explain as "monadic" a semantics that seems like default flattening to me. I am curious if you could define what you mean by "monadic". I don't much care if we call a promise system "monadic" or not, but let's not let a disagreement on this term obscure a possible agreement on what promises should do.
As I made clear in my "What Are We Arguing About" email, I want to separate the argument about default flattening (#0) from the argument about whether promises-for-promises are possible (#1), and from arguments about thenables and assimilation (#2,#3,#4).
AFAICT, leaving aside operations that would explicitly create promises-for-promises, i.e., "fulfill" (aka "accept"), I don't see how it is possible to create promises-for-promises with the remaining operations you and I seem to agree on: Q(x) (aka "Future.resolve(x)"), "then", "resolve", "reject". If promise-for-promises cannot be made in the first place, then no recursive unwrapping is required to take them apart. Specifically, assimilation aside and API details aside, what about Q do you disagree with, if anything?
If we can narrow our remaining disagreements down to #1..#7, that would be great progress. Yes, #1..#7 will still be a lot of work, but great progress nonetheless. And I will leave to you and other to fight about what is and isn't called "monadic" ;).
Here is a case where flattening helps:
function executeAndWaitForComplete(command) { return getJSON(commandUrl + command) .then(function (commandResult) { if (commandResult.complete) { return commandResult.model; } var statusUrl = commmandResult.statusUrl; return pollForCommandComplete(statusUrl); }) }
function pollForCommandComplete(statusUrl) { return new Future(function(resolver) { var poll = function (pollResult) { if (pollResult.done) { resolve.resolve(pollResult.model); } else { setTimeout(function() { getJSON(statusUrl).done(poll, resolver.reject); }, 500); } } getJSON(statusUrl).done(poll, resolver.reject); }); }
In this example, the server will receive a command from the client and will process it asynchronously. The client then needs to poll an endpoint to check for completion of the command. Just using Futures, flattening is helpful here. Without flattening, executeAndWaitForComplete would could return either a Future<object> OR return a Future<Future<object>>. In a non flattening world, the developer would have to change the function to:
function executeAndWaitForComplete(command) { return new Future(function(resolver) { getJSON(commandUrl + command) .done(function (commandResult) { if (commandResult.complete) { resolver.resolve(commandResult.model); } var statusUrl = commmandResult.statusUrl; pollForCommandComplete(statusUrl).done(resolver.resolve, resolver.reject); }, resolver.reject) }); }
With flattening, the first version is less code for the developer. With flattening its possible to run into odd errors without some kind of static analysis since JS is not type safe.
Ron
Sent from Windows Mail
From: Tab Atkins Jr. Sent: Friday, April 26, 2013 2:24 PM To: David Bruant Cc: Mark S. Miller, public-script-coord at w3.org, Mark Miller, Dean Tribble, es-discuss
On Fri, Apr 26, 2013 at 1:39 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Fri, Apr 26, 2013 at 6:36 AM, David Bruant <bruant.d at gmail.com> wrote:
Le 26/04/2013 14:54, Kevin Smith a écrit :
What exactly is the controversy here?
I think we all agree with the semantics of "then" as specified in Promises/A+. (If not, then we have a really big problem!)
If so, then the only real controversy is whether or not the API allows one to create a promise whose eventual value is itself a promise. Q does not: it provides only "resolve" and "reject". DOM Futures do by way of "Future.accept". As far as I know, there's nothing about Q's implementation that would make such a function impossible, it just does not provide one.
I believe at this point the question isn't so much "can I build a promise for a promise?", but rather "what should be the default Future semantics?" Namely:
Future.accept(5) .then(function(x){ return Future.accept(x); }) .then(function(y){ // is y a Future? })
I'm arguing in favor of y being guaranteed to be a non-Future value. It is my understanding others would want y to be a Future. That would be the controversy as I understand it.
No. Future callbacks can return Futures, which then chain (the return value of then adopts the state of the callback's return value). This is the big "monad" benefit that we keep talking about.
To lay it out even more clearly for any bystanders, in the following code:
getAFuture() .then(function(x) { return doSomeWork(x); }) .then(function(y) { // is y a Future? });
The answer to the question is "no", regardless of whether doSomeWork returns a plain value or a Future for a plain value.
If doSomeWork() returns a plain value, the future returned by the first .then() call accepts with that value. If doSomeWork() returns a future that eventually accepts, the future returned by the first .then() call waits until it accepts, and then also accepts with the same value.
This all happens without recursive unwrapping. It's just a feature of how this kind of thing works (it's a result of Futures matching the "monad" abstraction).
The only way that y will become a Future is if doSomeWork() explicitly and purposefully returns a future for a future for a value (in other words, Future<Future<x>>). In this case, the future returned by the
first .then() call waits until the outer Future from the return value finishes, then accepts with its value, and "this value" happens to be a Future<x>.
This sort of thing does not happen accidentally. It's hard to make nested futures unless you're doing it on purpose, or you have no idea what you're doing. In the first case, we want to trust the author, and in the second case, it's probably better for everyone involved if the code fails in a fairly obvious way, rather than attempting to paper over the problem. If you're competent and just doing the natural thing, the API naturally gives you plain values in callback arguments and singly-wrapped futures in return values.
This is why I've been arguing against recursive unwrapping in the general case.
On Sat, Apr 27, 2013 at 7:38 AM, Mark Miller <erights at gmail.com> wrote:
On Fri, Apr 26, 2013 at 1:51 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Fri, Apr 26, 2013 at 1:45 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
From: Tab Atkins Jr. [jackalmage at gmail.com]
Shorter me: this is why I keep asking people who want flattening to actually provide an example of where flattening is useful, that isn't (a) assimilation, (b) a result of weird language semantics from some non-JS language, or (c) an authoring error.
Since (multi-level) flattening only occurs for assimilation (per Promises/A+ 1.1), it appears we have been talking past each other. All examples of multi-level flattening will necessarily be examples of assimilation.
In that case, HUZZAH! We've solved the problem!
We may have.
When I argue for default flattening (#0 in my "What Are We Arguing About") and you argue against, you claim default flattening is not monadic, which I agree with. But then you go on to explain as "monadic" a semantics that seems like default flattening to me. I am curious if you could define what you mean by "monadic". I don't much care if we call a promise system "monadic" or not, but let's not let a disagreement on this term obscure a possible agreement on what promises should do.
Hmm, that's strange. I just mean "following the monad laws". In other words, Promises are monads, with .then as the monadic operation, taking a function of type "a -> Promise<b>", and resulting in a new
Promise. (For convenience, .then() is also the functor operation, allowing its function to be of type "a -> b", as long as "b" isn't a
Promise. But that doesn't harm the monad-ness, as long as you follow the laws.) Future.accept (or its equivalent in a final Promise spec) is the monadic lift operation.
We don't expose it explicitly (though we could), but the monadic "join" operation takes a Promise<Promise<a>>, and returns a new
promise that waits for both the inner and outer to accept, then accepts with the inner's state. If either rejects, the output promise rejects with the same reason.
Did you think I meant something else? If so, what? And, do you think I've made a mistake in describing the monadic properties of promises? If so, what?
As I made clear in my "What Are We Arguing About" email, I want to separate the argument about default flattening (#0) from the argument about whether promises-for-promises are possible (#1), and from arguments about thenables and assimilation (#2,#3,#4).
AFAICT, leaving aside operations that would explicitly create promises-for-promises, i.e., "fulfill" (aka "accept"), I don't see how it is possible to create promises-for-promises with the remaining operations you and I seem to agree on: Q(x) (aka "Future.resolve(x)"), "then", "resolve", "reject". If promise-for-promises cannot be made in the first place, then no recursive unwrapping is required to take them apart. Specifically, assimilation aside and API details aside, what about Q do you disagree with, if anything?
Correct - if you leave out the only promise constructor that can actually make promises-for-promises, you can't make promises-for-promises. ^_^ Thus, recursive unwrapping, outside of the assimilation use-case, isn't useful, as it's very hard to nest promises unless you're doing it on purpose. (And if you are doing it on purpose, it's potentially useful to have it act monadic.)
If we can narrow our remaining disagreements down to #1..#7, that would be great progress. Yes, #1..#7 will still be a lot of work, but great progress nonetheless. And I will leave to you and other to fight about what is and isn't called "monadic" ;).
No fights necessary, luckily - the monad laws are completely trivial, and it's easy to prove something does or doesn't obey them.
On Sat, Apr 27, 2013 at 9:21 AM, Ron Buckton <rbuckton at chronicles.org> wrote:
Here is a case where flattening helps: [snip example code] In this example, the server will receive a command from the client and will process it asynchronously. The client then needs to poll an endpoint to check for completion of the command. Just using Futures, flattening is helpful here. Without flattening, executeAndWaitForComplete would could return either a Future<object> OR return a Future<Future<object>>.
Nope, this is the same error that David Bruant was making in his arguments.
In the "monadic promise" proposal (as opposed to the "recursive unwrapping" proposal), a .then() callback can return a plain value or a promise for a plain value, and the output promise returned by .then() will be the exact same.
In other words, in your example code, executeAndWaitForComplete() gets a promise from getJSON(), then calls .then() to chain some code after it and returns the resulting promise. The .then() callback either returns commandResult.model (a plain value), or returns a promise generated from pollForCommandComplete() (which eventually completes with pollResult.model).
The only difference between the two code branches in .then() is that the first one (where it returns the model) accepts immediately with the model value, while the second one (where it returns a promise for the model) stays pending for a little while long, and then eventually accepts with the model value.
To anything calling executeAndWaitForComplete(), the return result is always Promise<model>. It is never Promise<Promise<model>>.
That's not something that can happen, because promises obey the monad laws, and that's how monads work.
(A quick primer - anything is a "monad" if it obeys some really simple laws. It has to be some wrapper class around a value (it's more abstract than that, actually, but talking about wrappers is easy), which exposes some function that takes a callback, typically called "bind" or "flatMap". Calling .bind() is very similar to calling .map() on an array - .map() takes a function, and calls it for every element in the array, making a new array with the return results. The only difference is that the callback to .bind() is expected to return a value in the same monad, so you get double-wrapping. For example, if you called .map() and only returned arrays, you'd get an array of arrays as the result. The magic here that makes it a monad is that the class knows how to "flatten" itself one level, so it can take the return result of .bind(), which has a double-wrapped value, and turn it into a single-wrapped value. This is how .then() works on Promises
- if you return a Promise<b> from the callback, you'll get a
Promise<Promise<x>>, but the Promise class knows how to flatten itself
by one level, so it does so and hands you back a simple Promise<x>.
To help drive the concept home, say we did "[1, 2, 3].map(x=>[x,
10x])". The return result would be "[[1,10],[2,20],[3,30]]". On the other hand, if we defined "bind" (/flatMap/chain/then) on Array, we could do the exact same thing "[1, 2, 3].bind(x=>[x, 10x])", but the
return result would be "[1, 10, 2, 20, 3, 30]" - it automatically flattens itself one level. (It's not obvious in this example, but it really does only do one level - if I'd returned [x, [10*x]] from the callback, the return from .bind() would be "[1, [10], 2, [20], 3, [30]]".))
Cool. I think we (at least you and I) have agreement on default flattening (#0).
FWIW, the reason I'm surprised that you're calling this monadic is the need for the dynamic test on 'as long as "b" isn't a Promise'. In other words, the signature of .then (including the receiver and excluding the errback) is overloaded as
promise<a> -> (a -> promise<b>) -> promise<b>
or promise<a> -> (a -> b) -> promise<b> // when b is not itself a promise
type
My impression is that each of these overloads by itself is a different monad operation and that testing the condition in the comment violates parametricity. If these do correspond to two different monad operations, then different laws cover each. A similar analysis applies to Q(x) (aka Future.resolve(x)).
In any case, no matter. We agree (assimilation aside) that this is how .then and Q should behave. Wonderful!
On Sat, Apr 27, 2013 at 9:48 AM, Mark Miller <erights at gmail.com> wrote:
Cool. I think we (at least you and I) have agreement on default flattening (#0).
Yay for terminology confusion masquerading as disagreement!
FWIW, the reason I'm surprised that you're calling this monadic is the need for the dynamic test on 'as long as "b" isn't a Promise'. In other words, the signature of .then (including the receiver and excluding the errback) is overloaded as
promise<a> -> (a -> promise<b>) -> promise<b> or promise<a> -> (a -> b) -> promise<b> // when b is not itself a promise type
My impression is that each of these overloads by itself is a different monad operation and that testing the condition in the comment violates parametricity. If these do correspond to two different monad operations, then different laws cover each. A similar analysis applies to Q(x) (aka Future.resolve(x)).
In any case, no matter. We agree (assimilation aside) that this is how .then and Q should behave. Wonderful!
The reason this is still monadic is because, as long as you follow the monad type requirements properly, passing a callback with signature "a -> Promise<b>", it works as a monad. No need for tests, no special
behavior, nothing.
It's just that we also mix in the functor behavior into .then(), so that, if you fail to satisfy the monad type requirements, we can fall back to treating it as a functor. That's the second overload. (It's not monadic.)
(It might be nice to explicitly provide a .map() operation following the functor laws, but it isn't very important to me.)
I'm fine with monadic operations that do additional things when you go beyond the basic type contract, just like I'm fine with calling Array.of() the monadic lift operator for Arrays - if called with one value, as the monad contract requires, it lifts the value into an array. It can just also be called with 0 or 2+ arguments, and that's fine.
On Sat, Apr 27, 2013 at 5:21 PM, Ron Buckton <rbuckton at chronicles.org> wrote:
Here is a case where flattening helps:
function executeAndWaitForComplete(command) { return getJSON(commandUrl + command) .then(function (commandResult) { if (commandResult.complete) { return commandResult.model;
This gets automatically lifted into Future, resulting in Future<typeof commandResult.model>.
} var statusUrl = commmandResult.statusUrl; return pollForCommandComplete(statusUrl);
This is always a Future and becomes the result of executeAndWaitForComplete with type Future<typeof commandResult.model>.
})
}
function pollForCommandComplete(statusUrl) { return new Future(function(resolver) { var poll = function (pollResult) { if (pollResult.done) { resolve.resolve(pollResult.model); } else { setTimeout(function() { getJSON(statusUrl).done(poll, resolver.reject); }, 500); } } getJSON(statusUrl).done(poll, resolver.reject); }); }
In this example, the server will receive a command from the client and will process it asynchronously. The client then needs to poll an endpoint to check for completion of the command. Just using Futures, flattening is helpful here. Without flattening, executeAndWaitForComplete would could return either a Future<object> OR return a Future<Future<object>>.
I think the major point of confusion in these discussions is the result of the framing of the discussion in terms of "flattening". I believe most beneficial viewpoint is that of "autolifting".
That is, the exceptional case is not when the function argument of "then" returns a Future+ that gets "flattened" but rather when the function argument of "then" returns a non-Future that gets automatically lifted into a Future.
This change in perspective is non-obvious because in many of these APIs there is no succinct lifting operation to make a Future from another value. This is a major reason why something like Future.of (Future.accept) is important.
In a non flattening world, the developer would have to change the function to:
function executeAndWaitForComplete(command) { return new Future(function(resolver) { getJSON(commandUrl + command) .done(function (commandResult) { if (commandResult.complete) { resolver.resolve(commandResult.model); } var statusUrl = commmandResult.statusUrl; pollForCommandComplete(statusUrl).done(resolver.resolve, resolver.reject); }, resolver.reject) }); }
In a non-flattening (and non-autolifting) world, this would be the code:
function executeAndWaitForComplete(command) { return getJSON(commandUrl + command) .then(function (commandResult) { if (commandResult.complete) { return Future.accept(commandResult.model); } var statusUrl = commmandResult.statusUrl; return pollForCommandComplete(statusUrl); }) }
The only difference here is that commandResult.model is explicitly wrapped. This is because the signature of then is actually Future<a> -> (a -> Future<b>) -> Future<b>.
On Sat, Apr 27, 2013 at 9:55 AM, David Sheets <kosmo.zb at gmail.com> wrote: [...]
I think the major point of confusion in these discussions is the result of the framing of the discussion in terms of "flattening". I believe most beneficial viewpoint is that of "autolifting".
That is, the exceptional case is not when the function argument of "then" returns a Future+ that gets "flattened" but rather when the function argument of "then" returns a non-Future that gets automatically lifted into a Future.
This change in perspective is non-obvious because in many of these APIs there is no succinct lifting operation to make a Future from another value. This is a major reason why something like Future.of (Future.accept) is important.
I was following you until this last paragraph. As you define autolifting in the first two paragraphs, Q(x) would be an autolifting operation. It has the signature:
promise<t> -> promise<t>
or t -> promise<t> // if t is not itself a promise type
Are you distinguishing "autolifting" vs "lifting"? If so, why do you think it is important or desirable to provide a lifting operation (as opposed to an autolifting operation)?
On Sat, Apr 27, 2013 at 10:05 AM, Mark S. Miller <erights at google.com> wrote:
Are you distinguishing "autolifting" vs "lifting"? If so, why do you think it is important or desirable to provide a lifting operation (as opposed to an autolifting operation)?
Because the "lifting" operation is the monadic lifting operation, which you need if you want to write monadic code that works predictably. If all you have is an auto-lifter, your code will randomly fail sometimes in mysterious ways, because you're violating the monad laws. (In a distinct, though thematically similar, way to how your code sometimes mysteriously fails if you use the Array constructor instead of Array.of().)
Sorry, I've been writing code with E style promises for, jeez, over 20 years now. (I suddenly feel very old :( .) I don't remember ever experiencing the failure you're talking about. Can you give a concrete example?
On Sat, Apr 27, 2013 at 6:05 PM, Mark S. Miller <erights at google.com> wrote:
On Sat, Apr 27, 2013 at 9:55 AM, David Sheets <kosmo.zb at gmail.com> wrote: [...]
I think the major point of confusion in these discussions is the result of the framing of the discussion in terms of "flattening". I believe most beneficial viewpoint is that of "autolifting".
That is, the exceptional case is not when the function argument of "then" returns a Future+ that gets "flattened" but rather when the function argument of "then" returns a non-Future that gets automatically lifted into a Future.
This change in perspective is non-obvious because in many of these APIs there is no succinct lifting operation to make a Future from another value. This is a major reason why something like Future.of (Future.accept) is important.
I was following you until this last paragraph. As you define autolifting in the first two paragraphs, Q(x) would be an autolifting operation. It has the signature:
promise<t> -> promise<t>
or t -> promise<t> // if t is not itself a promise type
Are you distinguishing "autolifting" vs "lifting"? If so, why do you think it is important or desirable to provide a lifting operation (as opposed to an autolifting operation)?
Yes. Autolifting is conditional on promise-ness. Lifting is fully parametric.
If the standard uses autolifting instead of recursive flattening, many of the headaches with "thenables" go away and we gain enormous flexibility in future interoperation with the spec.
For instance, if your code might manipulate objects which have callable "then" fields but which don't subscribe to the promises spec, it is safest to always use:
return Promise.of(myMaybeNonPromiseThenable);
This greatly reduces the criticality of the "is this a promise?" predicate because in most cases you will simply return a non-thenable (autolifted) or a promise-like thenable and not care. In those cases where you wish to put non-promise thenable inside of a promise or don't know if someone else will want to, the explicit use of the lifting operation lets you avoid autolifting/flattening.
This massively simplifies the protocol between the promises spec and those values it encapsulates by only ever making a single assumption that then-returned thenables are promise-like but their contents are totally opaque.
I believe this design results in the maximal flexibility and safety for the platform by supplying a handy autolifting "then" while also allowing people to easily subscribe to promise interaction (by then-returning a thenable), defend their thenables from magical unwrapping (by explicitly using Promise.of), and write completely polymorphic code.
With this design, in the most common case, developers won't have to use Promise.of. Perhaps the only common use will be in starting a promise chain from a constant:
var promise; if (just_use_a_constant) { promise = Promise.of(6); } else { promise = getAsyncValue(); } promise.then(function (x) { return x*2); });
While those who translate code from other languages, target their compilers to JS Promises, write polymorphic libraries, use non-promises with callable "then" fields, or invent new interoperable promise-like (but distinct) semantics won't have to worry about hacking around recursive unwrapping.
To me, having some standard for promise-like objects in the platform seems very fundamental for handling asynchrony, ordering, failure, need, and probability. If we consider promise-like objects as fundamental, we should investigate the properties of their operations:
With recursive flattening "then" operation, the time complexity of "then" is O(n) with n the number of nested promise-like objects. With autolifted "then" operation, the time complexity of "then" is O(1).
Here, I am using time complexity as a proxy for the mental complexity of the operation and not as a proxy for execution performance (recursive unwrapping is usually of static depth as we have seen). You can see that not only does the recursive flattening involve a hidden loop that the advanced programmer must reason about but also invokes the notion of "promise-like object" which, as we have seen, leads to all sorts of tangents regarding how to characterize the property of promise-ness and still maintain clarity, safety, extensibility, and ease-of-use.
I hope this explanation satisfies you. If not, I am more than happy to answer any questions you may have about this approach.
Warm ,
David Sheets
On Sat, Apr 27, 2013 at 10:20 AM, Mark Miller <erights at gmail.com> wrote:
Sorry, I've been writing code with E style promises for, jeez, over 20 years now. (I suddenly feel very old :( .) I don't remember ever experiencing the failure you're talking about. Can you give a concrete example?
E-style promises aren't monadic, so you'd never have the chance to use them with generic monadic libraries. ^_^
On Fri, Apr 26, 2013 at 11:18 AM, Andreas Rossberg <rossberg at google.com>wrote:
On 26 April 2013 16:25, Dean Landolt <dean at deanlandolt.com> wrote:
The fundamental controversy, as Juan just noted, is how to precisely identify a promise in order to do either of these two things. This problem isn't quite so clean cut, but it's much more important to solve. I've been trying to bring some attention to it over the last few days -- I hope it's clear that a
then
method is not enough to identify a promise language construct -- this will subtly break existing code (e.g. casperjs).Let me note that this is not the fundamental controversy (not for me, anyway). The fundamental controversy is whether there should be any irregularity at all, as is unavoidably introduced by implicit flattening. The problem you describe just makes the negative effect of that irregularity worse.
It may be a little late (I'm just catching up on these promise megathreads) but I was suggesting that the irregularity posed by flattening is only a distraction. AFAICT you're concern with flattening is actually in regard to resolution semantics, right? Otherwise it's unobservable whether you have a Promise<value> or a Promise<Promise<value>>. I'm trying to argue that given
a reliable branding a one-step resolver is easy and sensible to define (no flattening), and a recursive resolver is an obvious extension. Almost everyone would use the latter, but I completely agree that the former is necessary too. No irregularities, everybody wins.
I don't think I've been able to properly communicate the gravity of the branding issue. I'll try code:
getAFuture() .then(function(x) { return require('casper').create(); });
Assuming you know that factory returns a Casper instance, you would
probably expect a Promise<Casper> value, right? But Casper instances have
then
methods, so it'll quack like a promise to the Promises/A+
assimilation algorithm, so what gets returned will be a Promise<undefined>
value. You may luck out and notice some non-sensical behavior, but it's very possible this code could almost work just fine -- right until it doesn't. It's more than just a debug hazard -- this kind of code could easily slip into production even with awesome test coverage.
Casper's just a convenient example -- this problem applies to all
"thenables" that aren't Promises/A+ promises. What's worse, the semantics
of the word then
imply that many of these thenable objects will probably
almost work in the same way as the example above.
To my mind the question of flattening sorts itself out as soon as you define a means to reliably identify a promise. But it could also be sidestepped completely, which I believe is the option Dave Herman favored at one point...
It's been a few years but I recall an exchange we had where he took the
position that there shouldn't even be a method to test whether a value is a
promise -- IIRC he was arguing that any value | Promise<value>
functionality was unnecessary, and even hazardous. I was never clear on exactly how this could be made to work, especially in interoperable libraries, but as a language construct I suppose it's feasible. I'm curious to hear where Dave stands now -- he eventually built support for thenables into task.js (which I think was what sparked this exchange) but that could have been out of convenience. Of course, with this approach I can't imagine how promises could possibly be made nestable with one-step resolution (what people seem to be calling "monadic" now?).
On Sat, Apr 27, 2013 at 2:21 PM, Dean Landolt <dean at deanlandolt.com> wrote:
On Fri, Apr 26, 2013 at 11:18 AM, Andreas Rossberg <rossberg at google.com> wrote:
On 26 April 2013 16:25, Dean Landolt <dean at deanlandolt.com> wrote:
The fundamental controversy, as Juan just noted, is how to precisely identify a promise in order to do either of these two things. This problem isn't quite so clean cut, but it's much more important to solve. I've been trying to bring some attention to it over the last few days -- I hope it's clear that a
then
method is not enough to identify a promise language construct -- this will subtly break existing code (e.g. casperjs).Let me note that this is not the fundamental controversy (not for me, anyway). The fundamental controversy is whether there should be any irregularity at all, as is unavoidably introduced by implicit flattening. The problem you describe just makes the negative effect of that irregularity worse.
It may be a little late (I'm just catching up on these promise megathreads) but I was suggesting that the irregularity posed by flattening is only a distraction. AFAICT you're concern with flattening is actually in regard to resolution semantics, right? Otherwise it's unobservable whether you have a Promise<value> or a Promise<Promise<value>>. I'm trying to argue that given a reliable branding a one-step resolver is easy and sensible to define (no flattening), and a recursive resolver is an obvious extension. Almost everyone would use the latter, but I completely agree that the former is necessary too. No irregularities, everybody wins.
Given the history of this issue so far, and the persistent miscommunications, I'd like to make sure that you understand what is meant by non-recursive flattening, which is what we on the monad side are arguing for.
In a world with non-recursive flattening, like I want, this code:
getAPromise() .then(function(x) { return 5; }).then(function(y) { alert(y); });
and this code:
getAPromise() .then(function(x) { return Promise.accept(5); }).then(function(y) { alert(y); });
will both alert "5", not "<object Promise>".
The only way to get it to alert "<object Promise>" is with code like this:
getAPromise() .then(function(x) { return Promise.accept(Promise.accept(5)); }).then(function(y) { alert(y); });
That is, you have to very explicitly double up on Promises. It doesn't happen by accident.
It's only this last case, which should be rare unless you're doing it on purpose, which we're all arguing about. The recursive-flatten people want this case (and all higher-stacked cases, like "return Promise.accept(Promise.accept(Promise.accept(5)));") to all alert "5".
This breaks some useful invariants, though. It means that Promises are no longer monads, which prevents you from using monad libraries. It also means that you have to hack around the behavior in other cases
- if you omit a callback from .then(), it's supposed to just "pass through" for that case, so the next .then() call to provide the callback gets the exact same value as it would if the callback were moved up. This doesn't work with recursive-flattening, though - if you make a nested Promise explicitly, and then call .then(cb) on it, the callback will get a promise as an argument, but if you call .then().then(cb), the promise will have been flattened out. You have to insert a special hack into the resolution mechanics to make this not happen.
Since nested promises are hard to create unless you're doing it on purpose, can be useful, and produce more consistent semantics overall, we should be using non-recursive flattening. The only place where you want recursive flattening is when you're assimilating an outside-world promise from a foreign library, which should require an explicit action (so you don't end up "flattening" something like a Casper.js value, and getting nonsense out as a result). We can just follow Promises/A+'s flattening semantics, but invoke them from some explicit function. Best of all worlds.
It's been a few years but I recall an exchange we had where he took the position that there shouldn't even be a method to test whether a value is a promise -- IIRC he was arguing that any
value | Promise<value>
functionality was unnecessary, and even hazardous. I was never clear on exactly how this could be made to work, especially in interoperable libraries, but as a language construct I suppose it's feasible. I'm curious to hear where Dave stands now -- he eventually built support for thenables into task.js (which I think was what sparked this exchange) but that could have been out of convenience. Of course, with this approach I can't imagine how promises could possibly be made nestable with one-step resolution (what people seem to be calling "monadic" now?).
This is the position that E takes, but it has special language support for promises, and does the unwrapping by itself, automatically. That's fine, and it produces a non-monadic form of promises. JS likely can't do that, though - it's stuck with promises as "real" things, distinct from the values they wrap, so it should make the best of it and make them monadic.
(If you don't understand the term "monadic", look at my earlier post in this thread, where I gave a short primer. If you're still having trouble, let me know privately, and I'd be glad to explain it in more detail.)
I am worried that we're again separated by a common terminology more than we are by an actual technical disagreement. I am arguing against an unconditional lift operation that would make a promise-for-promise. Or at least seeking to provoke someone to provide a compelling example showing why this is useful[1]. What all the recent messages seem to be arguing for is the existence of a non-assimilating and perhaps a non-unwrapping lift-ish operation, so that one can make a promise-for-thenable. I have no objection to being able to make a promise-for-thenable.
The clearest sign to me of the potential for misunderstanding is the use of the term "flattening" for unwrapping of thenables. To be clear, "flattening" is about promises -- it is just the conditional autolifting seen from the other side (as I now understand the term autolifting -- thanks). "unwrapping" is what happens to thenables. "Assimilation" is recursive unwrapping of thenables. I understand that it can be difficult to keep these distinctions straight. I wouldn't be surprised if I've been sloppy myself with these terms earlier in these threads. But now that we're zeroing in, these fine distinctions matter.
As I demonstrated somewhere in one of the Promises/A+ threads, I don't think it is practical to prohibit these promises-for-thenables anyway. As an example, let's take Q's use of Q as an allegedly fully assimilating lift-ish function. Like an autolifter Q(promise) returns that promise. And Q(x) where x is neither a promise nor a thenable, returns promise-for-x. The controversial part -- which I fully sympathize with since it is a dirty hack -- is that Q(thenable) assimilates -- it does recursive unwrapping (NOT "flattening") of the thenable. Assimilation aside, Q is an autolifter, so I'll can it an "assimilating autolifter". Assume a system is which this Q function and .then are the only lift-ish operations, and that .then is also an assimilating autolifter. What guarantee is supposed to follow?
Assuming that the thenable check is if(typeof x.then === 'function'), intuitively, we might think that
Q(p).then(shouldntBeThenable => alert(typeof shouldntBeThenable.then));
should never alert 'function'. But this guarantee does not follow:
var p = {};
Q(p).then(shouldntBeThenable => alert(typeof shouldntBeThenable.then));
p.then = function() { return "gotcha"; };
This is a consequence of assimilation being a dirty hack. The notion of a thenable is only marginally more principled than the notion of array-like. It is not a stable property. Above, p became a thenable after Q already judged it to be a non-promise non-thenable and hence returned a promise-for-p. This promise scheduled a call to the callback before p became a thenable, but p became a thenable before the callback got called. Alleged guarantee broken.
Thus, no fundamental guarantee would be lost by introducing an expert-only operation that does autolifting but no unwrapping. This would make convenient what is possible anyway: promises-for-thenables. But this is not an argument for introducing a full lifting operation. Introducing that would break an important guarantee -- that there are no promises-for-promises. Without full lifting, promises-for-promises remain impossible.
I leave it to monad fans and/or haters of assimilation to suggest names for this convenient operation, a non-unwrapping autolifter. I'm confident that if I tried to name it, I'd only cause more confusion ;).
[1] FWIW, if there's interest, I can provide several examples where a promise-for-promise is useful, but I know of no compelling example. The utility of the examples I have in mind are not worth the cost of introducing this possibility of a promise-for-promise.
On Sat, Apr 27, 2013 at 3:49 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:
On Sat, Apr 27, 2013 at 2:21 PM, Dean Landolt <dean at deanlandolt.com> wrote:
On Fri, Apr 26, 2013 at 11:18 AM, Andreas Rossberg <rossberg at google.com> wrote:
On 26 April 2013 16:25, Dean Landolt <dean at deanlandolt.com> wrote:
The fundamental controversy, as Juan just noted, is how to precisely identify a promise in order to do either of these two things. This problem isn't quite so clean cut, but it's much more important to solve. I've been trying to bring some attention to it over the last few days -- I hope it's clear that a
then
method is not enough to identify a promise languageconstruct -- this will subtly break existing code (e.g. casperjs).
Let me note that this is not the fundamental controversy (not for me, anyway). The fundamental controversy is whether there should be any irregularity at all, as is unavoidably introduced by implicit flattening. The problem you describe just makes the negative effect of that irregularity worse.
It may be a little late (I'm just catching up on these promise megathreads) but I was suggesting that the irregularity posed by flattening is only a distraction. AFAICT you're concern with flattening is actually in regard to resolution semantics, right? Otherwise it's unobservable whether you have a Promise<value> or a Promise<Promise<value>>. I'm trying to argue that given a reliable branding a one-step resolver is easy and sensible to define (no flattening), and a recursive resolver is an obvious extension. Almost everyone would use the latter, but I completely agree that the former is necessary too. No irregularities, everybody wins.
Given the history of this issue so far, and the persistent miscommunications, I'd like to make sure that you understand what is meant by non-recursive flattening, which is what we on the monad side are arguing for.
In a world with non-recursive flattening, like I want, this code:
getAPromise() .then(function(x) { return 5; }).then(function(y) { alert(y); });
and this code:
getAPromise() .then(function(x) { return Promise.accept(5); }).then(function(y) { alert(y); });
will both alert "5", not "<object Promise>".
The only way to get it to alert "<object Promise>" is with code like this:
getAPromise() .then(function(x) { return Promise.accept(Promise.accept(5)); }).then(function(y) { alert(y); });
That is, you have to very explicitly double up on Promises. It doesn't happen by accident.
It's only this last case, which should be rare unless you're doing it on purpose, which we're all arguing about. The recursive-flatten people want this case (and all higher-stacked cases, like "return Promise.accept(Promise.accept(Promise.accept(5)));") to all alert "5".
This breaks some useful invariants, though. It means that Promises are no longer monads, which prevents you from using monad libraries. It also means that you have to hack around the behavior in other cases
- if you omit a callback from .then(), it's supposed to just "pass through" for that case, so the next .then() call to provide the callback gets the exact same value as it would if the callback were moved up. This doesn't work with recursive-flattening, though - if you make a nested Promise explicitly, and then call .then(cb) on it, the callback will get a promise as an argument, but if you call .then().then(cb), the promise will have been flattened out. You have to insert a special hack into the resolution mechanics to make this not happen.
Since nested promises are hard to create unless you're doing it on purpose, can be useful, and produce more consistent semantics overall, we should be using non-recursive flattening. The only place where you want recursive flattening is when you're assimilating an outside-world promise from a foreign library, which should require an explicit action (so you don't end up "flattening" something like a Casper.js value, and getting nonsense out as a result). We can just follow Promises/A+'s flattening semantics, but invoke them from some explicit function. Best of all worlds.
It's been a few years but I recall an exchange we had where he took the position that there shouldn't even be a method to test whether a value is a promise -- IIRC he was arguing that any
value | Promise<value>
functionality was unnecessary, and even hazardous. I was never clear on exactly how this could be made to work, especially in interoperable libraries, but as a language construct I suppose it's feasible. I'm curious to hear where Dave stands now -- he eventually built support for thenables into task.js (which I think was what sparked this exchange) but that could have been out of convenience. Of course, with this approach I can't imagine how promises could possibly be made nestable with one-step resolution (what people seem to be calling "monadic" now?).This is the position that E takes,
I'm not sure which piece "this" refers to, but in E it is possible to test whether a value is an unresolved promise. Testing this is discouraged, but is sometimes necessary for various meta-programming tasks, like serialization. It is not possible to test whether a value is a fulfilled promise, since a fulfilled promise really is indistinguishable from its fulfillment value.
On Sat, Apr 27, 2013 at 5:46 PM, Mark Miller <erights at gmail.com> wrote:
I am worried that we're again separated by a common terminology more than we are by an actual technical disagreement. I am arguing against an unconditional lift operation that would make a promise-for-promise. Or at least seeking to provoke someone to provide a compelling example showing why this is useful[1]. What all the recent messages seem to be arguing for is the existence of a non-assimilating and perhaps a non-unwrapping lift-ish operation, so that one can make a promise-for-thenable. I have no objection to being able to make a promise-for-thenable.
The clearest sign to me of the potential for misunderstanding is the use of the term "flattening" for unwrapping of thenables. To be clear, "flattening" is about promises -- it is just the conditional autolifting seen from the other side (as I now understand the term autolifting -- thanks). "unwrapping" is what happens to thenables. "Assimilation" is recursive unwrapping of thenables. I understand that it can be difficult to keep these distinctions straight. I wouldn't be surprised if I've been sloppy myself with these terms earlier in these threads. But now that we're zeroing in, these fine distinctions matter.
As I demonstrated somewhere in one of the Promises/A+ threads, I don't think it is practical to prohibit these promises-for-thenables anyway. As an example, let's take Q's use of Q as an allegedly fully assimilating lift-ish function. Like an autolifter Q(promise) returns that promise. And Q(x) where x is neither a promise nor a thenable, returns promise-for-x. The controversial part -- which I fully sympathize with since it is a dirty hack -- is that Q(thenable) assimilates -- it does recursive unwrapping (NOT "flattening") of the thenable. Assimilation aside, Q is an autolifter, so I'll can it an "assimilating autolifter".
Should be: '...so I'll call it an "assimilating autolifter".'.
On Sun, Apr 28, 2013 at 1:46 AM, Mark Miller <erights at gmail.com> wrote:
I am worried that we're again separated by a common terminology more than we are by an actual technical disagreement. I am arguing against an unconditional lift operation that would make a promise-for-promise. Or at least seeking to provoke someone to provide a compelling example showing why this is useful[1]. What all the recent messages seem to be arguing for is the existence of a non-assimilating and perhaps a non-unwrapping lift-ish operation, so that one can make a promise-for-thenable. I have no objection to being able to make a promise-for-thenable.
But promises aren't thenable? Why are they special?
The clearest sign to me of the potential for misunderstanding is the use of the term "flattening" for unwrapping of thenables. To be clear, "flattening" is about promises -- it is just the conditional autolifting seen from the other side (as I now understand the term autolifting -- thanks).
If (( f : a -> b => a -> Promise<b> if b != Promise<c> for all c
but f : a -> b => a -> b if b = Promise<c> for all c ) for all f under "then") is "autolifting"
Then (( f : a -> Promise<b> => a -> Promise<b> if b != Promise<c> for all c
but f : a -> Promise<b> => a -> Promise<c> if b = Promise<c> for all c ) for all f under "then") is "autojoining"
Because "join" : M M t -> M t
(properly, "autolifting" should convert a -> b into Promise<a> ->
Promise<b> and Promise<a> -> Promise<b> into Promise<a> -> Promise<b>
but we can probably ignore this earlier mis-take I made so long as we can agree on the behavior; "return autoboxing" might have been a better terminological choice)
"unwrapping" is what happens to thenables. "Assimilation" is recursive unwrapping of thenables. I understand that it can be difficult to keep these distinctions straight. I wouldn't be surprised if I've been sloppy myself with these terms earlier in these threads. But now that we're zeroing in, these fine distinctions matter.
I agree. We should adopt precise definitions to avoid further confusion. To that end, I propose we formally define "flattening" to be recursively autojoining to a fixpoint.
That is,
assimilation : thenable :: flattening : promise
This is because flattening implies things are, well, flat (one-level and no more).
As I demonstrated somewhere in one of the Promises/A+ threads, I don't think it is practical to prohibit these promises-for-thenables anyway. As an example, let's take Q's use of Q as an allegedly fully assimilating lift-ish function. Like an autolifter Q(promise) returns that promise. And Q(x) where x is neither a promise nor a thenable, returns promise-for-x. The controversial part -- which I fully sympathize with since it is a dirty hack -- is that Q(thenable) assimilates -- it does recursive unwrapping (NOT "flattening") of the thenable. Assimilation aside, Q is an autolifter, so I'll can it an "assimilating autolifter". Assume a system is which this Q function and .then are the only lift-ish operations, and that .then is also an assimilating autolifter. What guarantee is supposed to follow?
Let's use P<t> for the promise type constructor and T<t> for the
thenable type constructor.
Suppose we evaulate y = Q(x : P<T<P<T<P<t>>>>>). [which should really
never happen but ignore that, this is for edification]
Does Q simultaneously flatten and assimilate its argument to a fixpoint? Afaict, yes, because promises are a subset of thenables. Is y : P<t>?
I agree that Q is also overloaded as an autolifter such that Q : t ->
P<t> for simple t and Q : P<t> -> P<t> for simple t.
Assuming that the thenable check is if(typeof x.then === 'function'), intuitively, we might think that
Q(p).then(shouldntBeThenable => alert(typeof shouldntBeThenable.then));
should never alert 'function'. But this guarantee does not follow:
var p = {}; Q(p).then(shouldntBeThenable => alert(typeof shouldntBeThenable.then)); p.then = function() { return "gotcha"; };
This is a consequence of assimilation being a dirty hack. The notion of a thenable is only marginally more principled than the notion of array-like. It is not a stable property. Above, p became a thenable after Q already judged it to be a non-promise non-thenable and hence returned a promise-for-p. This promise scheduled a call to the callback before p became a thenable, but p became a thenable before the callback got called. Alleged guarantee broken.
Thus, no fundamental guarantee would be lost by introducing an expert-only operation that does autolifting but no unwrapping.
To do autolifting, you must have a predicate that distinguishes P<t> from T<t>.
This would make convenient what is possible anyway: promises-for-thenables. But this is not an argument for introducing a full lifting operation. Introducing that would break an important guarantee -- that there are no promises-for-promises. Without full lifting, promises-for-promises remain impossible.
This assumes that promises are somehow special and beyond construction by the programmer. This immaculate distinction troubles me because it immediately closes off the ability to construct systems which are peers to promises. That is,
promise.then(x => return unblessedPromise()).then(y =>
alert(isUnblessedPromise(y)))
alerts "true" because unBlessedPromise() was lifted into P<UnblessedPromise<t>> instead of being helpfully unwrapped and then
lifted into Promise<t>. (I should note here that dynamically
dispatching the subsequent "then" to UnblessedPromise<t> is an
interesting possible design decision and probably necessary to maintain associativity. This could be overridden by return unblessedPromise().then(Promise.of);)
That is the behavior of the above is identical to the behavior of
promise.then(x => return Promise.of(unblessedPromise())).then(y =>
alert(isUnblessedPromise(y)))
which is quite disappointing.
Ideally, any standardization of this facility would define a simple object protocol and a set of equivalences of operations over objects with types in that protocol's class. If a promise system which seamlessly interoperates with Official Promises cannot be constructed by a programmer (no object I can construct is true for the isPromise predicate), we will have lost a great deal. If this occurs, any JavaScript environment which enforces this blessed/unblessed distinction will no longer allow developers to construct many important libraries and compatible variations of promises (e.g. I want to centrally and conditionally enable logging for all promise fulfillments).
If you allow interoperability, then it must be decided how to signal what it is to be a promise and all promises must exhibit certain properties. In fact, I would go so far as to say that any /true/ standard should define both the contract of a promise-able and the specific semantics that its flavor of promise-able implements.
From this point of view, it is impossible to mandate no
promises-for-promises as there is, by definition, no way to tell if a given promise is yours or mine. This is an important invariant and crucial for the future health and extensibility of this feature.
Whether adherence to that contract occurs by checking isCallable(x.then) or in some other manner, it does not matter (except for compatibility with existing libraries which use that predicate as typeclass advertisement). In all circumstances, no recursive operations over promises or thenables should be mandated. Autojoining and autolifting are OK and, as you observe, lifting and joining cannot truly be prohibited. Assimilation or flattening are useful operation(s) when explicitly invoked; however, it should not occur in regular use of a promise.
I feel we have made great progress. I hope it is clear now that no built-in (automatically in, e.g., "then") unwrapping or assimilation should occur. If that is settled, the only remaining issue is over the signal to indicate that something is a promise whether built-in or user-defined.
Best wishes,
On Sun, Apr 28, 2013 at 3:41 AM, David Sheets <kosmo.zb at gmail.com> wrote:
On Sun, Apr 28, 2013 at 1:46 AM, Mark Miller <erights at gmail.com> wrote:
I am worried that we're again separated by a common terminology more than we are by an actual technical disagreement. I am arguing against an unconditional lift operation that would make a promise-for-promise. Or at least seeking to provoke someone to provide a compelling example showing why this is useful[1]. What all the recent messages seem to be arguing for is the existence of a non-assimilating and perhaps a non-unwrapping lift-ish operation, so that one can make a promise-for-thenable. I have no objection to being able to make a promise-for-thenable.
But promises aren't thenable? Why are they special?
The clearest sign to me of the potential for misunderstanding is the use of the term "flattening" for unwrapping of thenables. To be clear, "flattening" is about promises -- it is just the conditional autolifting seen from the other side (as I now understand the term autolifting -- thanks).
If (( f : a -> b => a -> Promise<b> if b != Promise<c> for all c but f : a -> b => a -> b if b = Promise<c> for all c ) for all f under "then") is "autolifting"
Then (( f : a -> Promise<b> => a -> Promise<b> if b != Promise<c> for all c but f : a -> Promise<b> => a -> Promise<c> if b = Promise<c> for all c ) for all f under "then") is "autojoining"
Because "join" : M M t -> M t
(properly, "autolifting" should convert a -> b into Promise<a> -> Promise<b> and Promise<a> -> Promise<b> into Promise<a> -> Promise<b> but we can probably ignore this earlier mis-take I made so long as we can agree on the behavior; "return autoboxing" might have been a better terminological choice)
"unwrapping" is what happens to thenables. "Assimilation" is recursive unwrapping of thenables. I understand that it can be difficult to keep these distinctions straight. I wouldn't be surprised if I've been sloppy myself with these terms earlier in these threads. But now that we're zeroing in, these fine distinctions matter.
I agree. We should adopt precise definitions to avoid further confusion. To that end, I propose we formally define "flattening" to be recursively autojoining to a fixpoint.
That is,
assimilation : thenable :: flattening : promise
This is because flattening implies things are, well, flat (one-level and no more).
As I demonstrated somewhere in one of the Promises/A+ threads, I don't think it is practical to prohibit these promises-for-thenables anyway. As an example, let's take Q's use of Q as an allegedly fully assimilating lift-ish function. Like an autolifter Q(promise) returns that promise. And Q(x) where x is neither a promise nor a thenable, returns promise-for-x. The controversial part -- which I fully sympathize with since it is a dirty hack -- is that Q(thenable) assimilates -- it does recursive unwrapping (NOT "flattening") of the thenable. Assimilation aside, Q is an autolifter, so I'll can it an "assimilating autolifter". Assume a system is which this Q function and .then are the only lift-ish operations, and that .then is also an assimilating autolifter. What guarantee is supposed to follow?
Let's use P<t> for the promise type constructor and T<t> for the thenable type constructor.
Suppose we evaulate y = Q(x : P<T<P<T<P<t>>>>>). [which should really never happen but ignore that, this is for edification]
Does Q simultaneously flatten and assimilate its argument to a fixpoint? Afaict, yes, because promises are a subset of thenables. Is y : P<t>?
I agree that Q is also overloaded as an autolifter such that Q : t -> P<t> for simple t and Q : P<t> -> P<t> for simple t.
Assuming that the thenable check is if(typeof x.then === 'function'), intuitively, we might think that
Q(p).then(shouldntBeThenable => alert(typeof shouldntBeThenable.then));
should never alert 'function'. But this guarantee does not follow:
var p = {}; Q(p).then(shouldntBeThenable => alert(typeof shouldntBeThenable.then)); p.then = function() { return "gotcha"; };
This is a consequence of assimilation being a dirty hack. The notion of a thenable is only marginally more principled than the notion of array-like. It is not a stable property. Above, p became a thenable after Q already judged it to be a non-promise non-thenable and hence returned a promise-for-p. This promise scheduled a call to the callback before p became a thenable, but p became a thenable before the callback got called. Alleged guarantee broken.
Thus, no fundamental guarantee would be lost by introducing an expert-only operation that does autolifting but no unwrapping.
To do autolifting, you must have a predicate that distinguishes P<t> from T<t>.
This would make convenient what is possible anyway: promises-for-thenables. But this is not an argument for introducing a full lifting operation. Introducing that would break an important guarantee -- that there are no promises-for-promises. Without full lifting, promises-for-promises remain impossible.
This assumes that promises are somehow special and beyond construction by the programmer. This immaculate distinction troubles me because it immediately closes off the ability to construct systems which are peers to promises. That is,
promise.then(x => return unblessedPromise()).then(y => alert(isUnblessedPromise(y)))
alerts "true" because unBlessedPromise() was lifted into P<UnblessedPromise<t>> instead of being helpfully unwrapped and then lifted into Promise<t>. (I should note here that dynamically dispatching the subsequent "then" to UnblessedPromise<t> is an interesting possible design decision and probably necessary to maintain associativity. This could be overridden by return unblessedPromise().then(Promise.of);)
That is the behavior of the above is identical to the behavior of
promise.then(x => return Promise.of(unblessedPromise())).then(y => alert(isUnblessedPromise(y)))
which is quite disappointing.
Ideally, any standardization of this facility would define a simple object protocol and a set of equivalences of operations over objects with types in that protocol's class. If a promise system which seamlessly interoperates with Official Promises cannot be constructed by a programmer (no object I can construct is true for the isPromise predicate), we will have lost a great deal. If this occurs, any JavaScript environment which enforces this blessed/unblessed distinction will no longer allow developers to construct many important libraries and compatible variations of promises (e.g. I want to centrally and conditionally enable logging for all promise fulfillments).
If you allow interoperability, then it must be decided how to signal what it is to be a promise and all promises must exhibit certain properties. In fact, I would go so far as to say that any /true/ standard should define both the contract of a promise-able and the specific semantics that its flavor of promise-able implements.
From this point of view, it is impossible to mandate no promises-for-promises as there is, by definition, no way to tell if a given promise is yours or mine. This is an important invariant and crucial for the future health and extensibility of this feature.
Whether adherence to that contract occurs by checking isCallable(x.then) or in some other manner, it does not matter (except for compatibility with existing libraries which use that predicate as typeclass advertisement). In all circumstances, no recursive operations over promises or thenables should be mandated. Autojoining and autolifting are OK and, as you observe, lifting and joining cannot truly be prohibited. Assimilation or flattening are useful operation(s) when explicitly invoked; however, it should not occur in regular use of a promise.
I feel we have made great progress. I hope it is clear now that no built-in (automatically in, e.g., "then") unwrapping or assimilation
s/unwrapping or assimilation/flattening or assimilation/ of course.
On Sat, Apr 27, 2013 at 5:46 PM, Mark Miller <erights at gmail.com> wrote:
I am worried that we're again separated by a common terminology more than we are by an actual technical disagreement. I am arguing against an unconditional lift operation that would make a promise-for-promise. Or at least seeking to provoke someone to provide a compelling example showing why this is useful[1]. What all the recent messages seem to be arguing for is the existence of a non-assimilating and perhaps a non-unwrapping lift-ish operation, so that one can make a promise-for-thenable. I have no objection to being able to make a promise-for-thenable.
The reasoning for an unconditional lift operator is that promises, as specified by DOM Futures or Promises/A+, are roughly a monad, and having an unconditional lift is required to satisfy the monad laws. Being a monad is useful, in ways similar to how being an iterable is useful, or an array-like, or many other typeclasses that various languages recognize.
For example, Future.all() is almost equivalent to a variadic liftA() (name taken from Haskell), which takes a function and any number of arguments, all wrapped in some particular monad, and executes the function with its arguments in the way that the monad prefers. For promises, that means "if every promise accepts, execute the function and return an accepted promise for the return value; otherwise, return a rejected promise". This sort of function comes for free once a class has been established as a monad, just as Python's very useful itertools library applies "for free" to anything that establishes itself as an iterable. You don't have to write any special code to make it happen; just the fact that promises are monads means that the generically-written liftA() method automatically works.
The clearest sign to me of the potential for misunderstanding is the use of the term "flattening" for unwrapping of thenables. To be clear, "flattening" is about promises -- it is just the conditional autolifting seen from the other side (as I now understand the term autolifting -- thanks). "unwrapping" is what happens to thenables. "Assimilation" is recursive unwrapping of thenables. I understand that it can be difficult to keep these distinctions straight. I wouldn't be surprised if I've been sloppy myself with these terms earlier in these threads. But now that we're zeroing in, these fine distinctions matter.
I'm not sure where you got this precise terminology, but I'm willing to adopt it for these discussions if it helps reduce confusion.
As I demonstrated somewhere in one of the Promises/A+ threads, I don't think it is practical to prohibit these promises-for-thenables anyway. As an example, let's take Q's use of Q as an allegedly fully assimilating lift-ish function. Like an autolifter Q(promise) returns that promise. And Q(x) where x is neither a promise nor a thenable, returns promise-for-x. The controversial part -- which I fully sympathize with since it is a dirty hack -- is that Q(thenable) assimilates -- it does recursive unwrapping (NOT "flattening") of the thenable. Assimilation aside, Q is an autolifter, so I'll can it an "assimilating autolifter". Assume a system is which this Q function and .then are the only lift-ish operations, and that .then is also an assimilating autolifter. What guarantee is supposed to follow?
.then() cannot be assimilating, nor can the default autolifter. Assimilation needs to be something explicit and separate, or else things will mysteriously break when you mix in objects like from Casper.js.
It's not cool, nor is it necessary, for promises to forever poison the landscape of "objects that happen to have a method named 'then'".
Assuming that the thenable check is if(typeof x.then === 'function'), intuitively, we might think that
Q(p).then(shouldntBeThenable => alert(typeof shouldntBeThenable.then));
should never alert 'function'. But this guarantee does not follow:
var p = {}; Q(p).then(shouldntBeThenable => alert(typeof shouldntBeThenable.then)); p.then = function() { return "gotcha"; };
This is a consequence of assimilation being a dirty hack. The notion of a thenable is only marginally more principled than the notion of array-like. It is not a stable property. Above, p became a thenable after Q already judged it to be a non-promise non-thenable and hence returned a promise-for-p. This promise scheduled a call to the callback before p became a thenable, but p became a thenable before the callback got called. Alleged guarantee broken.
Thus, no fundamental guarantee would be lost by introducing an expert-only operation that does autolifting but no unwrapping. This would make convenient what is possible anyway: promises-for-thenables. But this is not an argument for introducing a full lifting operation. Introducing that would break an important guarantee -- that there are no promises-for-promises. Without full lifting, promises-for-promises remain impossible.
Why do think that "no promises-for-promises" is an important guarantee?
I leave it to monad fans and/or haters of assimilation to suggest names for this convenient operation, a non-unwrapping autolifter. I'm confident that if I tried to name it, I'd only cause more confusion ;).
It should just be called "resolve", as in Future.resolve().
[1] FWIW, if there's interest, I can provide several examples where a promise-for-promise is useful, but I know of no compelling example. The utility of the examples I have in mind are not worth the cost of introducing this possibility of a promise-for-promise.
Given the demonstrated difficulty of accidentally creating a promise-for-a-promise, why do you feel it is so important to guarantee that it can't ever happen?
Thanks for the clarifications re: Future as a monad. My understanding of this is as follows (please correct me if I am wrong):
- The expected result of the resolve/reject callbacks passed to Future#then is itself a Future.
- If the result of the resolve/reject callbacks is not a Future it is logically lifted into a Future, by accepting the value on the Future that is the result of Future#then.
- The Future result of the callback is merged into the Future returned Future#then then by chaining to either Future#then (Future#done?) of the callback result.
- Given this, it is not usually necessary to "recursively unwrap" or "flatten" a Future. As a result Future#then should not flatten the result. This would preserve Future.accept(Future.accept(value)).then().then(F => /* F is Future(value) */).
I can also agree that it is inherently unsafe to assimilate "thenables" for Future.resolve/FutureResolver#resolve, due to possible collisions with the property name on existing objects such as with casper.js. This kind of collision is the same rationale for having an @iterator symbol for iterators, though I wouldn't think the same resolution is likely the best result in this case. I am of the opinion that native Futures should only resolve native Futures (or possibly their subclasses). To assimilate you could have a Future.of (or possibly Future.from to match Array.from for "array-like" objects), which would assimilate "future-like" objects (i.e. "thenables"), but only via one level of unwrapping and not recursively.
I'm still concerned about cancellation, but will spin up another thread to hopefully spark some thoughtful discussion on the topic.
Ron
On Mon, Apr 29, 2013 at 11:03 AM, Ron Buckton <rbuckton at chronicles.org> wrote:
Thanks for the clarifications re: Future as a monad. My understanding of this is as follows (please correct me if I am wrong):
- The expected result of the resolve/reject callbacks passed to Future#then is itself a Future.
- If the result of the resolve/reject callbacks is not a Future it is logically lifted into a Future, by accepting the value on the Future that is the result of Future#then.
- The Future result of the callback is merged into the Future returned Future#then then by chaining to either Future#then (Future#done?) of the callback result.
- Given this, it is not usually necessary to "recursively unwrap" or "flatten" a Future. As a result Future#then should not flatten the result. This would preserve Future.accept(Future.accept(value)).then().then(F => /* F is Future(value) */).
All correct. (Some of your precise mechanical details are probably wrong, but you got all the important bits.)
I can also agree that it is inherently unsafe to assimilate "thenables" for Future.resolve/FutureResolver#resolve, due to possible collisions with the property name on existing objects such as with casper.js. This kind of collision is the same rationale for having an @iterator symbol for iterators, though I wouldn't think the same resolution is likely the best result in this case. I am of the opinion that native Futures should only resolve native Futures (or possibly their subclasses). To assimilate you could have a Future.of (or possibly Future.from to match Array.from for "array-like" objects), which would assimilate "future-like" objects (i.e. "thenables"), but only via one level of unwrapping and not recursively.
I'm fine with the concept of using branding (via a symbol) to denote that something should be treated like a future. I'm also fine with only allowing Promises and subclasses. Whatever people decide is better.
Domenic, after a lot of experience, thinks that the assimilation procedure should be recursive (presumably "bottoming out" when it hits a native promise). I'm fine with that. It does mean that if you need to assimilate a thenable for a Casper object, it'll do the wrong thing, but that's the price you pay for arbitrary library compat. If you know your foreign thenable is only a single layer, you can safeguard its contents yourself, by chaining .then() and returning a native Promise holding the value. Then, the assimilation procedure will "eat" the thenable, hit the Promise and adopt its state, and the Casper object (or whatever) will be safe.
I'm still concerned about cancellation, but will spin up another thread to hopefully spark some thoughtful discussion on the topic.
Go for it. I have given some thought to it, and think I have a reasonable model.
-----Original Message----- From: Tab Atkins Jr. [mailto:jackalmage at gmail.com] Sent: Monday, April 29, 2013 11:21 AM To: Ron Buckton Cc: Mark Miller; David Sheets; Mark S. Miller; es-discuss; public-script- coord at w3.org; David Bruant; Dean Tribble Subject: Re: A Challenge Problem for Promise Designers
On Mon, Apr 29, 2013 at 11:03 AM, Ron Buckton <rbuckton at chronicles.org> wrote:
Thanks for the clarifications re: Future as a monad. My understanding of this is as follows (please correct me if I am wrong):
- The expected result of the resolve/reject callbacks passed to Future#then is itself a Future.
- If the result of the resolve/reject callbacks is not a Future it is logically lifted into a Future, by accepting the value on the Future that is the result of Future#then.
- The Future result of the callback is merged into the Future returned Future#then then by chaining to either Future#then (Future#done?) of the callback result.
- Given this, it is not usually necessary to "recursively unwrap" or "flatten" a Future. As a result Future#then should not flatten the result. This would preserve Future.accept(Future.accept(value)).then().then(F => /* F is Future(value) */).
All correct. (Some of your precise mechanical details are probably wrong, but you got all the important bits.)
I updated [1] my rough implementation of Future based on this discussion. This has the following changes from the previous [2] version which was based on the DOM spec for Future:
-
The resolver's resolve algorithm tests value to determine if it is a Future instance (rather than a "thenable"). This could later be done by checking branding or by checking for a symbol.
-
The resolver's resolve algorithm only unwraps the value once if it is a Future, rather than performing flattening. It does this by calling the resolver's accept algorithm in the "resolve" future callback for rather than the resolve algorithm.
-
In the steps for Future#then, if the "resolveCallback" is null, the "resolve" callback becomes a future callback for resolver and its accept algorithm. This is to preserve the value for something like:
Future.accept(Future.accept(value)).then().then(F => /* F is Future(value) /) Future.accept(Future.accept(Future.accept(value))).then().then(FF => / FF is Future(Future(value)) */)
-
In the steps for some/any/every, the future callbacks that are created that used the resolver's resolve algorithm now use the resolver's accept algorithm. This is to preserve the value for something like:
Future.any(Future.accept(Future.accept(value))).then(F => /* F is Future(value) /) Future.any(Future.accept(Future.accept(Future.accept(value)))).then(FF => / FF is Future(Future(value)) */)
-
Added Future.from to perform explicit assimilation (with only one level of unwrap, as with Future#then)
-
Added Future.isFuture to test for native Futures
Hopefully I've captured the mechanical details correctly in the implementation.
[1, updated implementation] rbuckton/promisejs/blob/master/Future1/Future.ts [2, spec implementation] rbuckton/promisejs/blob/master/Future0/Future.ts
Ron
On Mon, Apr 29, 2013 at 1:07 PM, Ron Buckton <rbuckton at chronicles.org> wrote:
I updated [1] my rough implementation of Future based on this discussion. This has the following changes from the previous [2] version which was based on the DOM spec for Future:
The resolver's resolve algorithm tests value to determine if it is a Future instance (rather than a "thenable"). This could later be done by checking branding or by checking for a symbol.
The resolver's resolve algorithm only unwraps the value once if it is a Future, rather than performing flattening. It does this by calling the resolver's accept algorithm in the "resolve" future callback for rather than the resolve algorithm.
In the steps for Future#then, if the "resolveCallback" is null, the "resolve" callback becomes a future callback for resolver and its accept algorithm. This is to preserve the value for something like:
Future.accept(Future.accept(value)).then().then(F => /* F is Future(value) /) Future.accept(Future.accept(Future.accept(value))).then().then(FF => / FF is Future(Future(value)) */)
In the steps for some/any/every, the future callbacks that are created that used the resolver's resolve algorithm now use the resolver's accept algorithm. This is to preserve the value for something like:
Future.any(Future.accept(Future.accept(value))).then(F => /* F is Future(value) /) Future.any(Future.accept(Future.accept(Future.accept(value)))).then(FF => / FF is Future(Future(value)) */)
This all sounds right.
- Added Future.from to perform explicit assimilation (with only one level of unwrap, as with Future#then)
Like I said, Domenic says that recursive assimilation is useful, and I'm inclined to believe him, as he has a lot more experience in getting arbitrary thenables to play nicely together than I do. ^_^
- Added Future.isFuture to test for native Futures
For the purpose of library code, you don't need this - just use "x instanceof Future". Future.isFuture is only useful for the language to define, so that it can tell something is a Future cross-frame.
-----Original Message----- From: Tab Atkins Jr. [mailto:jackalmage at gmail.com] Sent: Monday, April 29, 2013 1:20 PM To: Ron Buckton Cc: Mark Miller; David Sheets; Mark S. Miller; es-discuss; public-script- coord at w3.org; David Bruant; Dean Tribble Subject: Re: A Challenge Problem for Promise Designers
- Added Future.from to perform explicit assimilation (with only one level of unwrap, as with Future#then)
Like I said, Domenic says that recursive assimilation is useful, and I'm inclined to believe him, as he has a lot more experience in getting arbitrary thenables to play nicely together than I do. ^_^
I'll tinker with it and run some tests.
- Added Future.isFuture to test for native Futures
For the purpose of library code, you don't need this - just use "x instanceof Future". Future.isFuture is only useful for the language to define, so that it can tell something is a Future cross-frame.
The intent is to eventually have a rough polyfill for ES5 and earlier, so if Future.isFuture becomes part of the spec this would likely match using some kind of pseudo-symbol polyfill.
~TJ
Ron
Le 26/04/2013 13:24, Andreas Rossberg a écrit :
I'm adventuring myself in places where I don't have experience, but I dont think blocking is what has to happen. Programming languages I know all have a local-by-default semantics, that is all values being played with are expected to be local by default (which sort of makes sense for in single-machine environments). A programming language could take the opposite direction and consider all values as remote-by-default. If values are remote, with event-loop semantics, you never really block. Not more than is required to wait actual remote values (which is incompressible anyway). If you have local values abstracted as remote, the time you wait for the value is actually just shorter than when you're waiting for a value at the other side of the planet. Promise pipelining [1] allows to interact with values (or give the impression to) although they're not here yet.
I agree, it's certainly way too late to retrofit "remote by default values" in JS. But the thought experiment is interesting. Maybe to be tested in a compile-to-JS language.
True. I don't see that as an issue. Or more accurately, it doesn't make the non-compositional issue worse than it is today. However, providing both APIs, expert enough users will know what they should build on top of for the sake of good composability. Non-expert users will keep making non-composable libraries; I think even only providing the low-level API couldn't save them from themselves :-)
I don't know for others and about cakes too much, but I've had a ~8 months experience working with promises on a Node.js application and I've experienced the benefits of not worrying about nested promises and really enjoyed it. As a promise consumer (as opposed to a promise library author), I didn't miss promise<promise<T>> a single second. I wish the same experience for
whoever is using web platform promises. As many called out, where are the libraries with no-flattening semantics? Where is the experience with these libraries? What were the benefits and downsides of using these libraries? I believe libraries converged to flattening semantics out of experience. I believe (though can't prove it formally) that non-flattening may have existed but proved inefficient when used at scale. More on that below.
I can't answer definitively by lack of experience with non-flattening semantics + explicit flattening method, but I'm afraid it will result in boilerplate: Without flattening semantics, if a function changes of return value from Future<T> to Future<Future<T>>, then 2 things can happen:
Some will forget to call it before return, we can imagine that in JS, some functions will return both Future<T> and Future<Future<T>> on
different path (that's JavaScript, on the web, that'll happen, on purpose or by mistake) and suddenly, you end up calling the flattening function a lot (boilerplate). It wouldn't be surprising if blog posts or doc suggest to call it "just to be sure" at the beginning of every function taking a promise as argument (boilerplate as a good practice :-s). In the end, instead of calling it all the time "just to be sure" or being afraid that your code break because of a change in nesting depth, you just wish that it was all taken care of by the promise infrastructure (what I called "hiding under the carpet" in an earlier post)...
I can't speak for them, but I believe what I've described (starting at a function changing of signature or a function being able to return different promise nesting level) is the reason popular promise libraries converged to a flattening semantics.
David
[1] erights.org/elib/distrib/pipeline.html