- From: Tab Atkins Jr. <jackalmage@gmail.com>
- Date: Fri, 10 May 2013 13:00:59 -0700
- To: "Mark S. Miller" <erights@google.com>
- Cc: Jonas Sicking <jonas@sicking.cc>, Domenic Denicola <domenic@domenicdenicola.com>, "public-script-coord@w3.org" <public-script-coord@w3.org>, es-discuss <es-discuss@mozilla.org>
[Forgot to pick up es-discuss in this message. >_<] On Fri, May 10, 2013 at 11:42 AM, Tab Atkins Jr. <jackalmage@gmail.com> wrote: > On Fri, May 10, 2013 at 5:52 AM, Mark S. Miller <erights@google.com> wrote: >> Together with each of their explanations about what they meant. They are >> both right. Lifting and auto-lifting do not mix. Q Promises give us >> autolifting with no lifting. Monadic promises would give us lifting but no >> autolifting. Having both in one system creates a mess which will lead >> programmers into the particular pattern of bugs Jonas warns about in his >> message. > > (For the benefit of those confused, "auto-lifting" is what has been > termed elsewhere "conditional lifting". It's what Future.resolve() > does: if passed a promise, it returns a new promise chained from it > (as if you'd called p.then() with no arguments); if passed anything > else, it returns a new promise that immediately accepts with that > value.) > > I have no idea why you're trying to claim that "monadic promises" > gives no auto-lifting. Perhaps you've gotten yourself confused over > the proposals, and have reverted to a particularly strict view of what > "monadic promises" means? > > The proposal called "monadic promises" implies nothing more than that, > if the value returned by a .then() callback is a promise, one level of > unwrapping will occur. If you purposely return a nested promise, we > won't strip out *all* the levels, only the outermost. > > There's nothing about this that is incompatible with a conditional > lift operator, a la Future.resolve(). Nor is there any reason to > believe that the presence of Future.resolve() would lead to bugs. > This confuses me - adding Future.resolve() makes nested futures no > more likely (it can't create them), and *not* adding it makes some > code patterns (where you want to accept either a value or a promise > for the value, and work consistently in both situations) more > difficult to code for. > >> * A Monadic promise system would have Q.fulfill, p.map, and p.flatMap. >> >> * A Q-like promise system would have Q, and p.then >> >> * The dominant non-Q-like proposal being debated in these threads has >> Q.fulfill, Q, and p.then. >> >> This note explains why I believe the last is much worse than either of the >> other two choices. As Jonas points out, generic code, to be useful in this >> mixed system, has to be careful and reliable about how much it wraps or >> unwraps the payloads it handles generically. Had programmers been armed with >> .map and .flatMap, they could succeed reliably. Arming them only with .then >> will lead to the astray. As an example, let's start with a variant of Sam's >> async table abstraction. The parts in angle brackets or appearing as type >> declarations only documents what we imagine the programmer may be thinking, >> to be erased to get the real code they wrote. In this abstraction, only >> promises for keys are provided. The get operation immediately returns a >> promise for the value that will have been set. (An interesting variant is a >> table that works even when the get arrives first. But we can ignore this >> wrinkle here.) >> >> >> class AsyncTable<T,U> { >> constructor() { >> this.m = Map<T,U>(); // encapsulation doesn't matter for this >> example >> } >> set(keyP :Promise<T>, val :U) { >> keyP.then(key => { this.m.set(key, val) }); >> } >> get(keyP :Promise<T>) :Promise<U> { >> return keyP.then(key => this.m.get(key)); >> } >> } >> >> When U is a non-promise, the code above works as intended. The .then in the >> .get method was written to act as .map. When tested with U's that are not >> promises, it works fine. But when U is actually Promise<V>, the .get method >> above returns Promise<V> rather than Promise<U>. As far as the author is >> concerned, the .then is functioning as a broken .map in this case, with the >> signature >> >> p.then: Promise<T> -> (T -> U) -> Promise<V> // WTF? > > Yes, if you're writing generic code that may return either a plain > value or a promise, you have to wrap your return value in another > promise. The .then() magic (acting like either map() or flatMap(), > depending on the return value's type) is more convenient for normal > usage, but less convenient for people writing more generic code. > > The "unabashed monadic" version is more predictable for generic code, > but less convenient for normal code (you have to be sure of whether > your return value is a plain value or a promise, while .then() lets > you ignore that for the most part). > > The Q version (auto-flattening all the time, no nested promises) is > predictable both ways, but at the cost of erasing an entire class of > use-cases where you want to be able to interact with a promise > reliably without regard to what's inside of it. (In other words, it > becomes less predictable in regards to *time*, as what you thought > would just be a retrieval delay might turn into a larger delay while > the "inner" promise waits to be fulfilled.) > >> In a system with autolifting, you can't get full parametricity of promises >> simply by adding an unconditional lift. You have to remove, or at least >> strongly discourage, autolifting. Or you have to take pains to carefully >> work around it. >> >> The main failure mode of standards bodies is to resolve a conflict by adding >> the union of the advocated features. Here, this works even worse than it >> usually does. The coherence of lifting depends on the absence of >> autolifting, and vice versa. We need to make a choice. > > Nope, the correct way to write the code in the proposed version (with > .then() and both types of lifting) is: > > class AsyncTable<T,U> { > constructor() { > this.m = Map<T,U>(); > } > set(keyP :Promise<T>, val :U) { > keyP.then(key => { this.m.set(key, val) }); > } > get(keyP :Promise<T>) :Promise<U> { > return keyP.then(key => Q.fulfill(this.m.get(key))); > } > } > > Or, using Future terminology and erasing the types: > > class AsyncTable { > constructor() { > this.m = Map(); > } > set(keyP, val) { > keyP.then(key => { this.m.set(key, val) }); > } > get(keyP) { > return keyP.then(key => Future.accept(this.m.get(key))); > } > } > > (Alternately, we could wrap the value in a promise in the set() > function, and then just return it in the get() function. It doesn't > matter *where* the wrapping happens, just that it happens somewhere, > if you want to be generic.) > > And we can do even better! Let's make the two functions accept > promises *or* plain values as keys: > > class AsyncTable { > constructor() { > this.m = Map(); > } > set(key, val) { > Future.resolve(key).then(key => { this.m.set(key, val) }); > } > get(key) { > return Future.resolve(key).then(key => > Future.accept(this.m.get(key))); > } > } > > Easy and convenient! > > ~TJ
Received on Friday, 10 May 2013 20:01:47 UTC