The Paradox of Partial Parametricity
On Fri, May 10, 2013 at 5:55 AM, Mark S. Miller <erights at google.com> wrote: [...]
This same abstraction in a Q-like promise system would be written
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)); } }
Actually, it's even better:
class AsyncTable<t,u> {
constructor() {
this.m = Map<t,u>(); // encapsulation doesn't matter for this
example } set(keyP :Promise<t>, val :Ref<u>) { keyP.then(key => { this.m.set(key, val) }); } get(keyP :Promise<t>) :Promise<u> { return keyP.then(key => this.m.get(key)); } }
On Fri, May 10, 2013 at 6:05 AM, Mark S. Miller <erights at google.com> wrote:
On Fri, May 10, 2013 at 5:55 AM, Mark S. Miller <erights at google.com>wrote: [...]
This same abstraction in a Q-like promise system would be written
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)); } }
Actually, it's even better:
class AsyncTable<t,u> { constructor() { this.m = Map<t,u>(); // encapsulation doesn't matter for this
example } set(keyP :Promise<t>, val :Ref<u>) { keyP.then(key => { this.m.set(key, val) }); } get(keyP :Promise<t>) :Promise<u> { return keyP.then(key => this.m.get(key)); } }
The way one would actually write in Q is even better
class AsyncTable<t,u> {
constructor() {
this.m = Map<t,u>(); // encapsulation doesn't matter for this
example } set(keyP :Ref<t>, val :Ref<u>) { Q(keyP).then(key => { this.m.set(key, val) }); } get(keyP :Ref<t>) :Promise<u> { return Q(keyP).then(key => this.m.get(key)); } }
The difference is instructive. The Q version of Postel's law is to either require t or Ref<t>, and to either provide t or Promise<t>.
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)); } }
The way to make this work would be to lift the value stored in the map.
get(keyP :Promise<T>) :Promise<U> {
return keyP.then(key => Q.fullfill(this.m.get(key)));
}
Do you agree? Is your premise that forgetting such a "lawyer-ly" detail will amount to a foot-gun?
On Fri, May 10, 2013 at 6:58 AM, Kevin Smith <zenparsing at gmail.com> wrote:
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)); } }
The way to make this work would be to lift the value stored in the map.
get(keyP :Promise<T>) :Promise<U> { return keyP.then(key => Q.fullfill(this.m.get(key))); }
Do you agree? Is your premise that forgetting such a "lawyer-ly" detail will amount to a foot-gun?
It's more than my premise, it is my point ;).
If you think of .then as a being approximately .flatMap, then you might think to do this manually. But given the behavior of .then, programmers will as often think of it as .map-like as they will think of it as .flatMap-like. If code like the above is needed to use .then reliably, then its .map-like behavior is only a distraction leading people into using a behavior they cannot use reliably. In a system with .fulfill, what purpose is served by giving .then this dual role?
Le 10 mai 2013 à 14:55, Mark S. Miller <erights at google.com> a écrit :
[+es-discuss]
I didn't realize that I composed this in reply to a message only on public-script-coord. Further discussion should occur only on es-discuss. Sorry for the confusion.
On Fri, May 10, 2013 at 5:52 AM, Mark S. Miller <erights at google.com> wrote: I think the key exchange in these threads was
On Fri, May 3, 2013 at 4:17 PM, Jonas Sicking <jonas at sicking.cc> wrote: [...] I.e. permitting nested promises creates a more complex model and with that you always get more confusion and more questions.
On Sat, May 4, 2013 at 1:48 AM, Claus Reinke <claus.reinke at talk21.com> wrote: [...] From the perspective of a-promise-is-just-like-other-wrapper-classes, auto-flattening promises creates a more complex model
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 clarity I define the following APIs so I can define the three architectural choices in terms of the subset of these APIs they contain. Obviously, I do not intend to start a bikeshed yet on the particular names chosen for these operations. Let's stay focused on semantics, not terminology.
An upper case type variable, e.g. T, is fully parametric. It may be a promise or non-promise. A lower case type variable, e.g. t, is constrained to be a non-promise. If you wish to think in conventional type terms, consider Any the top type immediately split into Promise and non-promise. Thus type parameter t is implicitly constrained to be a subtype of non-promise.
Ref<T> is the union type of T and Promise<T>.
Q.fulfill(T) -> Promise<T> // the unconditional lift operator
Q(Ref<t>) -> Promise<t> // the autolift operator
p.map: Promise<T> -> (T -> U) -> Promise<U>
p.flatMap: Promise<T> -> (T -> Promise<U>) -> Promise<U> // If the onSuccess function returns a non-promise, this would throw an Error, // so this type description remains accurate for the cases which succeed.
p.then: Promise<T> -> (T -> Ref<u>) -> Promise<u> // Ignoring the onFailure callback
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.
Hello,
With this explanation, from my perspective (as someone who has never used promises, but rejoices in advance to use them), the Q-like model of promise seems far superior to me:
- lifting (for non-promises) + no-op (for promises) is advantageously replaced by one method, autolifting (working with both);
flatMap
(for promises) +map
(for non-promises) is advantageously replaced by one method,then
(working with both);
It follows that:
- it is impossible to have a promise for a promise, reducing the probability of bugs;
- it makes it easier to write generic algorithms that work for both non-promises and promises;
- in many situations, there is no need to ask oneself if one should provide a promise or a non-promise, reducing the burden of thinking to programmers and therefore the probability of bugs.
Not wanting to ask oneself «Should I provide a promise or a value?» is not sloppiness, but it is because promises are just uninteresting wrappers. I never ask myself: «Should I provide a string or a String object to the substring
method?», because it doesn't matter, and the String object is just an uninteresting wrapper. And to push the comparison further: it is dubious, and hopefully impossible, to wrap a String object in another String object, just like it is dubious to have a promise for a promise.
<snip>
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.
Indeed, the advantages I have mentioned vanished if we add unconditional lifting to autolifting + then
, because its bare use forces the programmer to think what type of object he should provide (a promise or a non-promise) in some situations. If you allow me to do a somewhat shaky comparison, the usefulness of ASI is greatly reduced by the few situations where ASI does not work; therefore most style guides recommend to not use ASI at all.
Following Tab's comments on the a Promise monad, I prototyped a Future library based on DOM Future using TypeScript. Since TS has no concept of a union type, I'm using TS overloads to approximate the Ref<T> union type example. It has roughly the following API:
class FutureResolver<T> {
accept(value: T): void;
resolve(value: Future<T>): void;
resolve(value: T): void;
reject(value: any): void;
}
class Future<T> {
constructor(init: (resolver: FutureResolver<T>) => void);
// unconditional lift
static accept<TResult>(value: TResult): Future<TResult>;
// autolift
static resolve<TResult>(value: TResult): Future<TResult>;
static resolve<TResult>(value: Future<TResult>): Future<TResult>;
static reject<TResult>(value: any): Future<TResult>;
// assimilation of thenables, similar to `Q()`. Assimilation stops at the first `Future`
static from<TResult>(value: any): Future<TResult>;
// autolifting then.
// for `p.map` like operation, `resolve` should return `Future.accept(u)`
// for `p.flatMap` like operation, `resolve` can return u or `Future.resolve(u)`, but no error is thrown
then<TResult>(resolve: (value: T) => Future<TResult>, reject: (value: any) => Future<TResult>): Future<TResult>;
then<TResult>(resolve: (value: T) => TResult, reject: (value: any) => Future<TResult>): Future<TResult>;
then<TResult>(resolve: (value: T) => Future<TResult>, reject: (value: any) => TResult): Future<TResult>;
then<TResult>(resolve: (value: T) => TResult, reject: (value: any) => TResult): Future<TResult>;
catch<TResult>(reject: (value: any) => Future<TResult>): Future<TResult>;
catch<TResult>(reject: (value: any) => TResult): Future<TResult>;
done(resolve: (value: T) => void, reject: (value: any) => void);
}
In the AsyncTable example you would write:
class AsyncTable<T, U> {
private m = new Map<T, U>();
set(keyP: Future<T>, val: U): void {
keyP.done(key => { this.m.set(key, val); });
}
get(keyP: Future<T>): Future<U> {
return keyP.then(key => Future.accept(this.m.get(key))); // accept causes an unconditional lift.
}
}
In Mark's second example, using the union type, you might instead have:
class AsyncTable<T, U> {
private m = new Map<T, Future<U>>();
set(keyP: Future<T>, val: Future<U>): void;
set(keyP: Future<T>, val: U): void;
set(keyP: Future<T>, val: any): void {
keyP.done(key => { this.m.set(key, Future.resolve(val)); }); // autolift `Ref<U>`-like union to `Future<U>`
}
get(keyP: Future<T>): Future<U> {
return keyP.then(key => this.m.get(key)); // no need for unconditional lift, `then` will merge the already auto-lifted `Future<U>`
}
}
And Mark's third example might be:
class AsyncTable<T, U> {
private m = new Map<T, Future<U>>();
// gah! TS needs union types...
set(keyP: Future<T>, val: Future<U>): void;
set(keyP: T, val: Future<U>): void;
set(keyP: Future<T>, val: U): void;
set(keyP: T, val: U): void;
set(keyP: any, val: any): void {
Future.resolve(keyP).done(key => { this.m.set(key, Future.resolve(val)); }); // autolift key and val
}
get(keyP: Future<T>): Future<U>;
get(keyP: T): Future<U>;
get(keyP: any): Future<U> {
return Future.resolve(keyP).then(key => this.m.get(key)); // autolift key, val is already a `Future<U>`
}
}
Ron
From: es-discuss-bounces at mozilla.org [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Claude Pache Sent: Friday, May 10, 2013 11:25 AM To: Mark S.Miller Cc: public-script-coord at w3.org; es-discuss Subject: Re: The Paradox of Partial Parametricity
Le 10 mai 2013 à 14:55, Mark S. Miller <erights at google.com<mailto:erights at google.com>> a écrit :
[+es-discuss]
I didn't realize that I composed this in reply to a message only on public-script-coord. Further discussion should occur only on es-discuss. Sorry for the confusion.
On Fri, May 10, 2013 at 5:52 AM, Mark S. Miller <erights at google.com<mailto:erights at google.com>> wrote:
I think the key exchange in these threads was
On Fri, May 3, 2013 at 4:17 PM, Jonas Sicking <jonas at sicking.cc<mailto:jonas at sicking.cc>> wrote: [...] I.e. permitting nested promises creates a more complex model and with that you always get more confusion and more questions.
On Sat, May 4, 2013 at 1:48 AM, Claus Reinke <claus.reinke at talk21.com<mailto:claus.reinke at talk21.com>> wrote: [...]
From the perspective of a-promise-is-just-like-other-wrapper-classes,
auto-flattening promises creates a more complex model
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 clarity I define the following APIs so I can define the three architectural choices in terms of the subset of these APIs they contain. Obviously, I do not intend to start a bikeshed yet on the particular names chosen for these operations. Let's stay focused on semantics, not terminology.
An upper case type variable, e.g. T, is fully parametric. It may be a promise or non-promise. A lower case type variable, e.g. t, is constrained to be a non-promise. If you wish to think in conventional type terms, consider Any the top type immediately split into Promise and non-promise. Thus type parameter t is implicitly constrained to be a subtype of non-promise.
Ref<T> is the union type of T and Promise<T>.
Q.fulfill(T) -> Promise<T> // the unconditional lift operator
Q(Ref<t>) -> Promise<t> // the autolift operator
p.map: Promise<T> -> (T -> U) -> Promise<U>
p.flatMap: Promise<T> -> (T -> Promise<U>) -> Promise<U> // If the onSuccess function returns a non-promise, this would throw an Error, // so this type description remains accurate for the cases which succeed.
p.then: Promise<T> -> (T -> Ref<u>) -> Promise<u> // Ignoring the onFailure callback
-
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.
Hello,
With this explanation, from my perspective (as someone who has never used promises, but rejoices in advance to use them), the Q-like model of promise seems far superior to me:
- lifting (for non-promises) + no-op (for promises) is advantageously replaced by one method, autolifting (working with both);
flatMap
(for promises) +map
(for non-promises) is advantageously replaced by one method,then
(working with both);
It follows that:
- it is impossible to have a promise for a promise, reducing the probability of bugs;
- it makes it easier to write generic algorithms that work for both non-promises and promises;
- in many situations, there is no need to ask oneself if one should provide a promise or a non-promise, reducing the burden of thinking to programmers and therefore the probability of bugs.
Not wanting to ask oneself «Should I provide a promise or a value?» is not sloppiness, but it is because promises are just uninteresting wrappers. I never ask myself: «Should I provide a string or a String object to the substring
method?», because it doesn't matter, and the String object is just an uninteresting wrapper. And to push the comparison further: it is dubious, and hopefully impossible, to wrap a String object in another String object, just like it is dubious to have a promise for a promise.
<snip>
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.
Indeed, the advantages I have mentioned vanished if we add unconditional lifting to autolifting + then
, because its bare use forces the programmer to think what type of object he should provide (a promise or a non-promise) in some situations. If you allow me to do a somewhat shaky comparison, the usefulness of ASI is greatly reduced by the few situations where ASI does not work; therefore most style guides recommend to not use ASI at all.
On 10 May 2013 14:52, Mark S. Miller <erights at google.com> wrote:
An upper case type variable, e.g. T, is fully parametric. It may be a promise or non-promise. A lower case type variable, e.g. t, is constrained to be a non-promise. If you wish to think in conventional type terms, consider Any the top type immediately split into Promise and non-promise. Thus type parameter t is implicitly constrained to be a subtype of non-promise.
Mark, I'm afraid such a distinction makes absolutely no sense in a world of structural types. How would you specify the set of "types that aren't promises" in a way that is compatible with structural subtyping?
[Forgot to pick up es-discuss in this message. >_<]
[dropping public-script-coord. Let's keep the discussion on es-discuss. Apologies again on starting this thread with an email to the wrong list.]
On Fri, May 10, 2013 at 1:00 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:
[Forgot to pick up es-discuss in this message. >_<]
On Fri, May 10, 2013 at 11:42 AM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Fri, May 10, 2013 at 5:52 AM, Mark S. Miller <erights at 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.
[...]
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.
[...]
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.
[...]
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.
Hi Tab, I did not intend to start a fight with you over the term "monadic promises". Since we seem to be otherwise in agreement on the three architectures to be compared as well as many of the implications of each, I'm happy to call the first bullet above "unabashed monadic promises", the second "Q-like promises" and the third "abashed monadic promises". Is this acceptable? If not, please suggest something you expect we can all find acceptable. AFAICT, this is the last terminology battle that still distracts us from semantics.
One request: If you do suggest alternate terminology, don't call #3 "monadic promises" while at the same time calling #1 "<adjective> monadic
promises". That would make #1 seem like a subtype of #3. Either prefix both with an adjective or make them otherwise distinct.
I did not intend to start a fight
I didn't intend to start a fight either. ^_^ I just wanted to make sure no one was arguing against strawmen.
If you do suggest alternate terminology, don't call #3 "monadic promises" while at the same time calling #1 "<adjective> monadic promises". That would make #1 seem like a subtype of #3. Either prefix both with an adjective or make them otherwise distinct.
I don't think we need to refer to "unabashed monadic promises" (your #1) at all - nobody's seriously defending them, and they're far from Promises/A+. It's trivial to add map()
and flatMap()
to promises if you want them, but .then()
functions as a perfectly fine flatMap()
already if you maintain type discipline yourself.
The only serious proposals seem to be "Q-like promises", which explicitly prevent nested promises by auto-flattening when necessary, and "monadic promises", which allow nested promises if you explicitly ask for it (but the flattening semantics ensure that it's hard to accidentally fall into a nested case).
these are the three architectures I am trying to compare in order to make a point. Since you don't like the labels I've chosen for them, please suggest alternatives that should be clear and acceptable to all so that we can resume the conversation. Thanks.
Nobody is suggesting or wants a "pure" monadic option, so it's unimportant what you call it.
Fine. If there are no further objections, the three architectures are
- Unabashed Monadic Promises
- Q-Like Promises
- Abashed Monadic Promises
I found it a fun exercise to show how little code it takes to build unabashed monadic promises on top of Q-like promises. (It's been a while since I exercised those brain-muscles, so any corrections appreciated.) The punch line is
function unit(x) {
return Q({ x });
}
function bind(m, f) {
return m.then({ x } => f(x));
}
My interpretation of this exercise---apart from the fact that I miss doing mathematical proofs---is that, since it's so little code to implement unabashed monadic promises on top of Q-like promises, and Q-like promises have proven their worth in JavaScript whereas unabashed monadic promises have not, it makes much more sense to standardize on Q-like promises as the base, and leave unabashed monadic promises to user-space libraries.
(Abashed monadic promises are, of course, a failure mode of standardization---as Mark points out---and not really worth considering.)
On Wed, May 22, 2013 at 6:04 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
I found it a fun exercise to show how little code it takes to build unabashed monadic promises on top of Q-like promises. (It's been a while since I exercised those brain-muscles, so any corrections appreciated.) The punch line is
function unit(x) { return Q({ x }); } function bind(m, f) { return m.then({ x } => f(x)); }
My interpretation of this exercise---apart from the fact that I miss doing mathematical proofs---is that, since it's so little code to implement unabashed monadic promises on top of Q-like promises, and Q-like promises have proven their worth in JavaScript whereas unabashed monadic promises have not, it makes much more sense to standardize on Q-like promises as the base, and leave unabashed monadic promises to user-space libraries.
(Abashed monadic promises are, of course, a failure mode of standardization---as Mark points out---and not really worth considering.)
It's a weird wrapper object whose sole reason for existing is to defeat the auto-unwrapping. That's so ugly. ;_;
Try this proposal out instead:
Promises stack. Nothing magical, they're just containers that can contain anything, including more promises.
.then() waits until it can resolve to a plain value (delaying until nested promises resolve) before calling its callbacks. The callback return values have the current spec magic, where you can return either a plain value or a promise, and in the latter case the chained promise unwraps it once and adopts its state. You can return nested promises here, but you'll never see them as long as you use .then().
.chain() has the same signature, but doesn't wait - it calls its callbacks as soon as it resolves, with whatever value is inside of it, whether it's a plain value or another promise. Callback return values have to be promises, or else it throws. (This means that it's the monadic operation, with no caveats.)
The remaining two methods, .done() and .catch(), match .then() for convenience/conceptual integrity.
This proposal keeps the nice, conceptual simplicity of the promises model, and gives you a choice of how to handle it, whether you want the immediate underlying value or just the plain value after they all resolve. You can mix and match mid-stream if you'd like; you're not locked into one or the other as soon as you start using one of them (unlike the use-a-wrapper proposal).
This is, I think, a minimal surface-area, maximum ability proposal, which addresses both the single-unwrapping use-cases and the recursive-unwrapping convenience factor at the same time. The methods that people may be familiar with from their existing promise usage is the "convenient" one with the nice magic, while "chain" matches the nascent monadic efforts in JS and has a nice distinct name (which I think communicates better about the behavior, too, where you have to return a promise).
Thoughts?
Tab’s proposal as I understand it is to standardize Q-like promises and add a "chain" method that is "then" but with behavior tailored for monadic composition.
This sounds like a good compromise.
The only downside I can contrive is that it gives users, particularly novices, a subtle choice. Would it be more clear that it is intended for monadic composition if the name were literally "bind"?
Kris Kowal
It also adds a fulfill method. Thus, it presents two interfaces to the user: fulfill + chain (aka unit + bind), and Q + then (aka resolve + then). This seems to squarely fall into the trap Mark described in his original post, viz.
The main failure mode of standards bodies is to resolve a conflict by adding the union of the advocated features.
And indeed, I think the subsequent sentences ring just as true:
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.
The point of my post was to demonstrate that fulfill/chain aka unit/bind could be built in user space extremely simply, thus allowing "the nascent monadic efforts in JS" to go off and do their own thing for a few years before asking to be baked into the platform. Promises, in the Q-plus-then-sense, have paid their dues. It's not very sporting for the monadic efforts to hijack the promise-standardization train, without first doing similar due diligence via real-world implementations and experience.
On Thu, May 23, 2013 at 6:19 AM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
The point of my post was to demonstrate that fulfill/chain aka unit/bind could be built in user space extremely simply, thus allowing "the nascent monadic efforts in JS" to go off and do their own thing for a few years before asking to be baked into the platform.
JS is a compiler target. You are demanding that languages with parametric polymorphism create extra garbage to map their semantics onto your magic. You are demanding that authors who wish to create parametric libraries jump through hoops and uglify their interfaces to provide simple invariants.
Promises, in the Q-plus-then-sense, have paid their dues.
Have they? Which languages have promises-in-the-Q-sense baked in?
It's not very sporting for the monadic efforts to hijack the promise-standardization train, without first doing similar due diligence via real-world implementations and experience.
Haskell. OCaml. Any language with parametric polymorphism and static types.
Actual arguments are preferable over unsubstantiated assertions and attempts at marginalization.
On Wed, May 22, 2013 at 9:31 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
From: Tab Atkins Jr. [mailto:jackalmage at gmail.com]
Thoughts?
Sounds like a great user-space library!!
You... you can't. You can't build .chain() on top of .then() in a way that actually interoperates. You'll just get two different types of promises which can't talk to each other.
You can do the opposite, interestingly, because .chain() doesn't lose data, so a .then() method can be defined by the author to just do the necessary chaining for you until it ends up with a plain value.
On Wed, May 22, 2013 at 10:19 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
It also adds a fulfill method. Thus, it presents two interfaces to the user: fulfill + chain (aka unit + bind), and Q + then (aka resolve + then). This seems to squarely fall into the trap Mark described in his original post, viz.
The main failure mode of standards bodies is to resolve a conflict by adding the union of the advocated features.
Certainly a common failure mode, but it's not always a failure mode. Sometimes, there really are two distinct use-cases for something, and you need two solutions for it. Obviously, I believe this falls into that bucket, but others may disagree. ^_^
In this case, it's easy to demonstrate that you can fulfill either use-case starting from the other one. .chain() lets .then() be created in user-space in a way such that both styles of interaction work together. .then() lets .chain() be created in user-space by splitting things into "thenable promises" and "chainable promises", which can't interoperate without ugliness.
I was gracious and
And indeed, I think the subsequent sentences ring just as true:
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.
This one doesn't even make sense to me. "Autolifting" (aka conditional lifting, aka Future#resolve) is very useful for monads. I'm not sure where Mark gets the idea that lifting is incoherent if autolifting exists. I'd appreciate being corrected if I'm wrong.
(If autolifting is your only operation, then adding lifting is incoherent, I agree, because you've already established that it's impossible to have nested containers. But the reverse does not hold true.)
The point of my post was to demonstrate that fulfill/chain aka unit/bind could be built in user space extremely simply, thus allowing "the nascent monadic efforts in JS" to go off and do their own thing for a few years before asking to be baked into the platform. Promises, in the Q-plus-then-sense, have paid their dues. It's not very sporting for the monadic efforts to hijack the promise-standardization train, without first doing similar due diligence via real-world implementations and experience.
As David points out, trying to paint monads as something new and unproven is pretty obviously silly. They've been proving themselves for 20+ years.
On Thu, May 23, 2013 at 6:57 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:
On Wed, May 22, 2013 at 10:19 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
It also adds a fulfill method. Thus, it presents two interfaces to the user: fulfill + chain (aka unit + bind), and Q + then (aka resolve + then). This seems to squarely fall into the trap Mark described in his original post, viz.
The main failure mode of standards bodies is to resolve a conflict by adding the union of the advocated features.
Certainly a common failure mode, but it's not always a failure mode. Sometimes, there really are two distinct use-cases for something, and you need two solutions for it. Obviously, I believe this falls into that bucket, but others may disagree. ^_^
In this case, it's easy to demonstrate that you can fulfill either use-case starting from the other one. .chain() lets .then() be created in user-space in a way such that both styles of interaction work together. .then() lets .chain() be created in user-space by splitting things into "thenable promises" and "chainable promises", which can't interoperate without ugliness.
I was gracious and
And indeed, I think the subsequent sentences ring just as true:
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.
This one doesn't even make sense to me. "Autolifting" (aka conditional lifting, aka Future#resolve) is very useful for monads. I'm not sure where Mark gets the idea that lifting is incoherent if autolifting exists. I'd appreciate being corrected if I'm wrong.
I went into more depth on this matter in a presentation I just did at TC39. The slide deck is at strawman:promisesvsmonads2.pdf. It was written to accompany a verbal explanation, which it did, but not to be self-explanatory. I will try to find the time to explain it, but not today. I'll be posting more on this soon. Nevertheless, I think y'all will get something out of the slideshow prior to this explanation.
TC39 did not yet come to an official consensus. That said, the emerging winner in the room was clearly AP3 from slide19 of the slide deck, which Alex has revised Promise.idl to follow. It was clear from the positions in the room that we needed both lifting and autolifting in order to achieve consensus. In the room I was agreeable to AP3 as well. I remain so.
AP3 only does recursive unwrapping on the return side of .then. AP2, based on your post from yesterday, does recursive unwrapping on both sides. I stated that I prefer the AP2 style recursive unwrapping on both sides, but the advocates for use of .fulfill (unconditional lifting) strongly favored AP3. I will let them speak for themselves.
Even after sleeping on it, I can live happily with AP3. It is actually even better at supporting QP style than I appreciated yesterday. In fact, for good QP style code, it is equivalent to AP2. I am satisfied that either in AP2 or AP3, autolifting is coherent in the presence of lifting since the presence of lifting will be invisible to good QP style code.
As for whether lifting is coherent in the presence of autolifting in AP3, I will let the lifting advocates speak for themselves.
We may not have made the best choice, but we didn't make sausage.
(If autolifting is your only operation, then adding lifting is incoherent, I agree, because you've already established that it's impossible to have nested containers. But the reverse does not hold true.)
The point of my post was to demonstrate that fulfill/chain aka unit/bind could be built in user space extremely simply, thus allowing "the nascent monadic efforts in JS" to go off and do their own thing for a few years before asking to be baked into the platform. Promises, in the Q-plus-then-sense, have paid their dues. It's not very sporting for the monadic efforts to hijack the promise-standardization train, without first doing similar due diligence via real-world implementations and experience.
As David points out, trying to paint monads as something new and unproven is pretty obviously silly. They've been proving themselves for 20+ years.
~TJ
-- Cheers, --MarkM -------------- next part -------------- An HTML attachment was scrubbed... URL: esdiscuss/attachments/20130524/8c1338c9/attachment
On Thu, May 23, 2013 at 10:18 PM, Mark S. Miller <erights at google.com> wrote:
I went into more depth on this matter in a presentation I just did at TC39. The slide deck is at strawman:promisesvsmonads2.pdf. It was written to accompany a verbal explanation, which it did, but not to be self-explanatory. I will try to find the time to explain it, but not today. I'll be posting more on this soon. Nevertheless, I think y'all will get something out of the slideshow prior to this explanation.
TC39 did not yet come to an official consensus. That said, the emerging winner in the room was clearly AP3 from slide19 of the slide deck, which Alex has revised Promise.idl to follow. It was clear from the positions in the room that we needed both lifting and autolifting in order to achieve consensus. In the room I was agreeable to AP3 as well. I remain so.
AP3 only does recursive unwrapping on the return side of .then. AP2, based on your post from yesterday, does recursive unwrapping on both sides. I stated that I prefer the AP2 style recursive unwrapping on both sides, but the advocates for use of .fulfill (unconditional lifting) strongly favored AP3. I will let them speak for themselves.
Even after sleeping on it, I can live happily with AP3. It is actually even better at supporting QP style than I appreciated yesterday. In fact, for good QP style code, it is equivalent to AP2. I am satisfied that either in AP2 or AP3, autolifting is coherent in the presence of lifting since the presence of lifting will be invisible to good QP style code.
As for whether lifting is coherent in the presence of autolifting in AP3, I will let the lifting advocates speak for themselves.
We may not have made the best choice, but we didn't make sausage.
Unfortunately, AP3 is incoherent. I'd've thought you'd like it less. ^_^
AP3 (recursive unwrapping of the return value of .then()) doesn't give Q people the guarantees they want (.then() callbacks always receive a plain value), nor does it give monadic people the flexibility they want (Domenic's proposal is, I'm sorry, abhorrent in its details - I'll provide more reasoning at the end of this email). It also adds a useless fulfill method - you can't actually use the fact that the promises are nested at all, which is just plain silly. AP3 is straight-up consensus-exhaustion, where people weren't thinking the details through sufficiently to realize the details weren't coherent.
The correct solution is AP2, but with recursive unwrapping on the read side for .then().
This is much better than AP3. You note that AP2 and AP3 are equivalent for "good QP style code". If you flip it so it just unwraps on the read side, it's equivalent for all QP-style code - even if the surrounding code is passing around nested promises like it's going out of style, your QP-style code can safely rely on the guarantee that your callbacks will never receive a promise as their argument.
What's more, if you're using .then() for things, but you're passing in a callback from elsewhere that assumes it can return nested promises, everything's still kosher! If you chain another .then() off of it, you'll still get a plain value; if you chain a .chain()/.flatMap() off of it, you'll get the "real" return value of the inner promise. This wins for everyone, in every situation I've been able to think of.
I'm sorry that I'm not a TC39 member, and that the talks were prepared before the conversations earlier this week that led me to discover this particular combination that works best. I think I could've influence the conversation better had I been there. :/
Details about why Domenic's solution is unfortunately terrible:
-
It only looks remotely passable because it relies on argument destructuring to mostly hide the fact that it's abusing a useless wrapper object solely to defeat the recursive unwrapping. If you removed that and used ES5 argument lists, the ugliness would be manifest.
-
Domenic uses "{x}" in his example code, which looks like a standard placeholder argument name, but it's not. "x" is actually part of the contract of the proposal, and must be used by all callbacks to interoperate. If they want to name their argument something else, they have to use the longer "{x: foo}" form, making it less convenient and more obviously hacky. (In your slides, you change the name to "value".)
-
Domenic's "promises" are, in practice, a completely different beast from normal promises, which don't interoperate at all. If you've got a .then-based workflow, and a method tries to return a Domenic-promise, you'll just break - you're expecting a plain value, not an object with a .value property containing the actual plain value (or maybe containing another object with .value, containing another object with .value, containing the plain value). The same applies in reverse - calling .chain/.flatMap() on a normal promise either fails (because you only put the function on a subclass) or it breaks (because you're trying to decompose the argument, but it's a plain value, not an object with a .value method).
Tab Atkins Jr. wrote:
What's more, if you're using .then() for things, but you're passing in a callback from elsewhere that assumes it can return nested promises, everything's still kosher! If you chain another .then() off of it, you'll still get a plain value; if you chain a .chain()/.flatMap() off of it, you'll get the "real" return value of the inner promise. This wins for everyone, in every situation I've been able to think of.
I couldn't attend the third day of the TC39 meeting, but from Mark's slides, I take it the solution -- or let's say "coping strategy" -- with AP3 is to always wrap {value: payload} if you want to chain.
Mark, is that right?
I'm sorry that I'm not a TC39 member, and that the talks were prepared before the conversations earlier this week that led me to discover this particular combination that works best. I think I could've influence the conversation better had I been there. :/
Not to worry, no final ultimate double-checked decision was reached. ;-)
On Fri, May 24, 2013 at 12:40 PM, Brendan Eich <brendan at mozilla.com> wrote:
Tab Atkins Jr. wrote:
What's more, if you're using .then() for things, but you're passing in a callback from elsewhere that assumes it can return nested promises, everything's still kosher! If you chain another .then() off of it, you'll still get a plain value; if you chain a .chain()/.flatMap() off of it, you'll get the "real" return value of the inner promise. This wins for everyone, in every situation I've been able to think of.
I couldn't attend the third day of the TC39 meeting, but from Mark's slides, I take it the solution -- or let's say "coping strategy" -- with AP3 is to always wrap {value: payload} if you want to chain.
Mark, is that right?
Yeah, that's right, but like I keep saying, that means that "thenable promises" and "chainable promises" are no longer interoperable without ugly helpers - a chainable promise might stack promises (doing {value:{value:{value:foo}}}), and if you call .then() on it, you end up receiving that ugly stacked object, rather than the "foo" that you want.
In my proposal, it all works great - you can start with chainable promises and then call .then() on it, you get the internal "foo" immediately, because .then() waits for all its internal promises to resolve. It can do this because it knows what the internal promises are - they aren't some anonymous object designed solely to defeat the ability of a promise to recognize them as promises.
I'm sorry that I'm not a TC39 member, and that the talks were prepared before the conversations earlier this week that led me to discover this particular combination that works best. I think I could've influence the conversation better had I been there. :/
Not to worry, no final ultimate double-checked decision was reached. ;-)
Yeah, I know, but I've learned over time that influencing the conversation early is useful. ^_^
Tab Atkins Jr. wrote:
Yeah, that's right, but like I keep saying, that means that "thenable promises" and "chainable promises" are no longer interoperable without ugly helpers - a chainable promise might stack promises (doing {value:{value:{value:foo}}}), and if you call .then() on it, you end up receiving that ugly stacked object, rather than the "foo" that you want.
Yup. AP3 is deficient in this way, AP2 is not.
Tab Atkins Jr. wrote:
Not to worry, no final ultimate double-checked decision was reached.;-)
Yeah, I know, but I've learned over time that influencing the conversation early is useful. ^_^
Forgot to write: "early"? LOL, this past week's meeting was years late in terms of promises language/library design and implementation. Also in terms of all the mega-threads on Futures/Promises that made me sleepy.
Let's say "right on time", especially if we can course-correct to AP2.
On Fri, May 24, 2013 at 1:42 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
AP3 (recursive unwrapping of the return value of .then()) doesn't give Q people the guarantees they want (.then() callbacks always receive a plain value), nor does it give monadic people the flexibility they want (Domenic's proposal is, I'm sorry, abhorrent in its details - I'll provide more reasoning at the end of this email). It also adds a useless fulfill method - you can't actually use the fact that the promises are nested at all, which is just plain silly. AP3 is straight-up consensus-exhaustion, where people weren't thinking the details through sufficiently to realize the details weren't coherent.
Since the minutes aren't available yet (thanks to Arv for working on that), I'll say a little about what happened in the meeting. As the person who spoke out the most, I think, for the proposals that unfortunately go under the "monadic" rubric, my #1 concern is that promises can be used to build compositional APIs. This means API like a future async-local-storage where no restriction on the domain of values to "non-promises" is required for the API to work correctly. I think is significantly more important than whether the promise API is monadic in the sense you've advocated for, although I'd certainly like that too.
In the meeting, there were (a) people advocating for styles of programming along the lines that you (Tab) have put forward, like me, (b) people advocating for Q-style programming, like Mark, Yehuda, and Tom, and (c) genuine concern about the complexity budget of the promise API, expressed most by Luke Hoban, but felt by all of us. Were it not for this last concern, I think AP2 would have seemed more attractive. But since the set of methods available on Promises themselves is the most visible part of the API, and AP2 doubles its size, that told heavily against AP2. Given that, my personal preference would have been for an API that never did recursive unwrapping, but given the wide existing precedent for .then() doing multiple levels of unwrapping, it seems like that semantics would require a second method on all promises.
This line of reasoning led everyone in the room to the point that Mark reported. This isn't the point that anyone would have chosen from first principles, but I do think that as Mark says, it allows everyone to get most of what they want without falling into a union-of-features result.
Sam Tobin-Hochstadt wrote:
In the meeting, there were (a) people advocating for styles of programming along the lines that you (Tab) have put forward, like me, (b) people advocating for Q-style programming, like Mark, Yehuda, and Tom, and (c) genuine concern about the complexity budget of the promise API, expressed most by Luke Hoban, but felt by all of us. Were it not for this last concern, I think AP2 would have seemed more attractive. But since the set of methods available on Promises themselves is the most visible part of the API, and AP2 doubles its size, that told heavily against AP2.
AP2 from Mark's slides:
AP2 (Based on Tab’s latest) • Q.fulfill // lifting • Q() // autolifting, resolve • p.then // deep flattening • p.flatMap // “chain”
AP3, next slide:
AP3 • Q.fulfill // lifting • Q() // autolifting, resolve • p.then // deep flattening of returned value
There's no doubling, just 4 methods instead of 3. If we can't do what you preferred, lose the recursive unwrapping, then either users hack {value: x} workarounds and sometimes end up with bugs where such wrappers were not manually unwrapped -- or we add chain. I'm with Tab on this.
On Sat, May 25, 2013 at 1:30 PM, Brendan Eich <brendan at mozilla.com> wrote:
Sam Tobin-Hochstadt wrote:
In the meeting, there were (a) people advocating for styles of programming along the lines that you (Tab) have put forward, like me, (b) people advocating for Q-style programming, like Mark, Yehuda, and Tom, and (c) genuine concern about the complexity budget of the promise API, expressed most by Luke Hoban, but felt by all of us. Were it not for this last concern, I think AP2 would have seemed more attractive. But since the set of methods available on Promises themselves is the most visible part of the API, and AP2 doubles its size, that told heavily against AP2.
AP2 from Mark's slides:
AP2 (Based on Tab’s latest) • Q.fulfill // lifting • Q() // autolifting, resolve • p.then // deep flattening • p.flatMap // “chain”
AP3, next slide:
AP3 • Q.fulfill // lifting • Q() // autolifting, resolve • p.then // deep flattening of returned value
There's no doubling, just 4 methods instead of 3.
This is twice as many methods on promises themselves: then
vs then
and chain
. Many more people will consume the promise datatype than
produce it, which mean that this does double the methods that
consumers need to know about.
Sam Tobin-Hochstadt wrote:
This is twice as many methods on promises themselves:
then
vsthen
andchain
.
Oh, methods :-P.
I'm an FPer at heart, can you tell?
Many more people will consume the promise datatype than produce it, which mean that this does double the methods that consumers need to know about.
Either they need to know about one method and the {value: x} hackaround, which is needed sometimes; or they need to know about two methods, then and chain. Which is worse? You could argue the {value: x} hackaround is rarely needed, but the same argument can sell chain too. I just don't see how bean-counting is the right way to assess here.
The problem {value: x} solves is real, no one dismisses it. In arguing about how to enable developers to solve it, we should count all the beans and try to assign weights to them. My weighted counting favors a second method over a manual wrapping pattern. Tab made this point already in arguing that then + chain is not a union of designs, any more than then + {value: x} is unioning in the bad committee-designed sense. From everything you wrote, though, it sounds like TC39 people just bean-counted methods. Boo...
/be
On Sat, May 25, 2013 at 10:30 AM, Brendan Eich <brendan at mozilla.com> wrote:
Sam Tobin-Hochstadt wrote:
In the meeting, there were (a) people advocating for styles of programming along the lines that you (Tab) have put forward, like me, (b) people advocating for Q-style programming, like Mark, Yehuda, and Tom, and (c) genuine concern about the complexity budget of the promise API, expressed most by Luke Hoban, but felt by all of us. Were it not for this last concern, I think AP2 would have seemed more attractive. But since the set of methods available on Promises themselves is the most visible part of the API, and AP2 doubles its size, that told heavily against AP2.
AP2 from Mark's slides:
AP2 (Based on Tab?s latest)
- Q.fulfill // lifting
- Q() // autolifting, resolve
- p.then // deep flattening
- p.flatMap // ?chain?
Apologies for being a broken record, but just to make sure the detail isn't lost in the noise...
AP2 is almost right. If .then() only flattens on the read side, it's perfect. The difference is undetectable to code that sticks strictly with .then(), but it allows for easier composition with .chain()-based code.
For example, a .then() callback can just return a promise from some other function. This other function could be returning a nested promise (it might be a database which can hold promises, for example), and so a .chain() off of it can usefully just unwrap the DB promise and deal with the inner promise. If .then() flattens on the return side too, this possibility is lost - the unwrapping behavior infects the chained future regardless of how you interact with it.
I'm ambivalent about whether .flatMap/chain() should be properly monadic (rejecting the chained promise with a TypeError if you return a non-promise from the callback) or have the same loose behavior as .then() where it allows you to return a non-promise and figures out what you mean. I lean toward the former because .then() exists already, but wouldn't die if the latter were chosen.
Tab Atkins Jr. wrote:
AP2 from Mark's slides:
AP2 (Based on Tab’s latest) • Q.fulfill // lifting • Q() // autolifting, resolve • p.then // deep flattening • p.flatMap // “chain”
Apologies for being a broken record, but just to make sure the detail isn't lost in the noise...
AP2 isalmost right. If .then() only flattens on the read side, it's perfect.
With you all the way on this one.
Who proposes flattening the return value ("write side", right)?
Just one more from the peanut gallery. I'm a big fan of the Q library. I use it in production code. I like the auto-unwrapping and feel like it works in a way that I expect. I like E and greatly respect Mark's work and opinion as well. I can't really think of any situations where I would need nested promises. Still, in this debate I find myself coming around to Tab's proposal, including his details about flattening on the read side. I strongly come down on the path of using thens and recursive unwrapping as the happy path that everyone should really use unless they know what they're doing. Still, the additional method does not come at a high cost in my opinion. If there is going to be a standard "hacky workaround", then the additional method seems pretty straightforward and elegant.
On the other hand, if nobody is going to actually need the method, then I guess it may very well be dead weight. I'm sorry if I have just missed it trying to keep up to date, but what are the compelling use cases. I mostly understand the monad thing, I can recognize them when I see them - I've used haskell. Is the only reason so that a monadic style can be used, with promises included? Perhaps that's enough, but I guess I'd really like to hear something more direct. Also, I'd like to know how many people are really using monadic JavaScript in production or if its mostly just a hypothetical scenario.
Finally, if we wish to add the method and we worry about the burden on developers who have no interest in monads, perhaps a method name that really comes from their perspective, or one that feels more experts only would seem less burdening. Something like "resolveOnce" or "forceInvokeNext" or "resolveNonGreedy" or even "monadicThen". Some of those are probably more acceptable than others, but I think that "chain" will too likely seem useful to people who won't want to use it. JavaScript developers, I think have expectations about what chain means. For most people, they with just think of wrapping something for method chaining.
On Sun, May 26, 2013 at 7:30 AM, Russell Leggett <russell.leggett at gmail.com> wrote:
On the other hand, if nobody is going to actually need the method, then I guess it may very well be dead weight. I'm sorry if I have just missed it trying to keep up to date, but what are the compelling use cases. I mostly understand the monad thing, I can recognize them when I see them - I've used haskell. Is the only reason so that a monadic style can be used, with promises included? Perhaps that's enough, but I guess I'd really like to hear something more direct. Also, I'd like to know how many people are really using monadic JavaScript in production or if its mostly just a hypothetical scenario.
The fact that we can make it monadic is just a bonus; any time you see "monad" just think "container++" - it's just a proven useful bit of structure on top which makes it easy to work with the values inside the container. (It's a bit more abstract than that, of course, but thinking in terms of containers works for many monads, and helps guide understanding toward the more abstract notion.)
The use-cases for "generic container" promises have been explored. Many types of promises are fairly interchangable, because all they're doing is providing a tiny async barrier between you and the result, and so collapsing them together isn't very objectionable. Some types of promises, though, represent something significant and different - significant delays, significant computation, etc. (A big class of these "significant and different" promises will be lazy promises, which don't do their computation at all until someone registers a callback for the first time.) You don't want to automatically resolve them just because they were handed to you wrapped in another promise, or at least, you don't always want this; sometimes, you'll want to instead work with it as a promise, so you're not incurring extra delay until you actually need their inner value.
Finally, if we wish to add the method and we worry about the burden on developers who have no interest in monads, perhaps a method name that really comes from their perspective, or one that feels more experts only would seem less burdening. Something like "resolveOnce" or "forceInvokeNext" or "resolveNonGreedy" or even "monadicThen". Some of those are probably more acceptable than others, but I think that "chain" will too likely seem useful to people who won't want to use it. JavaScript developers, I think have expectations about what chain means. For most people, they with just think of wrapping something for method chaining.
I don't think single-unwrapping is so painful as to require a painful name to go along with it. Plus, given that we can make it monadic, the value comes from using a name generic enough to be useful for other monadic structures, so as we add more, the network effects accrue automatically (rather than requiring a library that just adds a bunch of aliases to various classes).
Russell Leggett wrote:
I'm sorry if I have just missed it trying to keep up to date, but what are the compelling use cases.
AsyncTable with promises as values.
No name mangling. I prefer then and chain. One more method doesn't break anyone's brain, in view of the acknowledged {value: x} manual-wrapping/unwrapping alternative.
2013/5/26 Brendan Eich <brendan at mozilla.com>
Russell Leggett wrote:
I'm sorry if I have just missed it trying to keep up to date, but what are the compelling use cases.
AsyncTable with promises as values.
What the discussion at last week's TC39 meeting clarified for me is the following:
- Promises are primarily a control-flow abstraction.
- Sometimes, they are also used as a data-abstraction (i.e. as a container in their own right, wrapping an arbitrary payload).
- All of the subtle problems discussed in these threads only start to arise when these two use cases of promises are being mixed, e.g. a promise-as-data-container being mistaken for a promise-as-control-abstraction. This rarely happens in application code, but may happen in generic library code.
The AsyncTable is one example of such generic library code that uses both promises-as-data and promises-for-control.
Personally, after 6 years of programming with futures/promises, I have almost never used promises-as-data. I use them exclusively for control. I suspect most proponents of the Q-style of using promises share that experience.
And to add why auto-unwrapping is the right thing to do when using promises-for-control:
I think of promise/resolve as the async analog to function call/return.
In sequential programming, imagine a function f() that calls another function g() and returns its result (i.e. the call to g() is a tail-call). The callers of f() need not know or care that f used a nested call/return to provide its result.
In async programming, imagine a function f() that asynchronously calls another function g() and returns its promise-result. The async callers of f(), who immediately receive a promise for f's result, need not know or care that f used a nested async call/return to provide that result. Any .then callback registered on f's promise wants to see the value returned by g, not a promise.
When using promises-as-data, auto-unwrapping becomes the wrong thing to do. Hence the clash in semantics. I hope that is a good summary of the debate.
On Sun, May 26, 2013 at 1:38 PM, Brendan Eich <brendan at mozilla.com> wrote:
Russell Leggett wrote:
I'm sorry if I have just missed it trying to keep up to date, but what are the compelling use cases.
AsyncTable with promises as values.
No name mangling. I prefer then and chain. One more method doesn't break anyone's brain, in view of the acknowledged {value: x} manual-wrapping/unwrapping alternative.
I'm just going to go ahead and play stupid here. Why is it called chain? I agree one more method doesn't break anyone's brain, but I think the confusion comes when it is not clear what a method is for and when they should use it. Can anyone just try to write a really quick API doc for chain so that someone without knowledge of monads could read it? It should hopefully be fairly obvious after reading the doc why the method is called chain.
On 27 May 2013 15:30, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
What the discussion at last week's TC39 meeting clarified for me is the following:
- Promises are primarily a control-flow abstraction.
- Sometimes, they are also used as a data-abstraction (i.e. as a container in their own right, wrapping an arbitrary payload).
- All of the subtle problems discussed in these threads only start to arise when these two use cases of promises are being mixed, e.g. a promise-as-data-container being mistaken for a promise-as-control-abstraction. This rarely happens in application code, but may happen in generic library code.
Well, the gist of higher-order languages is that control flow abstractions are data. For example, that's the defining characteristics of first-class functions. And while most functions won't be used in a first-class manner (and most programmers probably don't think about them that way), the ability to do so gives great power -- as JavaScript demonstrates very well.
Futures/promises are an abstraction for first-class synchronisation. For the same reason you sometimes want to store or pass back & forth functions, you will sometimes want to store or pass promises. Not being able to combine those abstractions freely and transparently would arbitrarily limit their power, and practically demote promises to second-class status.
On Mon, May 27, 2013 at 7:29 AM, Russell Leggett <russell.leggett at gmail.com> wrote:
I'm just going to go ahead and play stupid here. Why is it called chain? I agree one more method doesn't break anyone's brain, but I think the confusion comes when it is not clear what a method is for and when they should use it. Can anyone just try to write a really quick API doc for chain so that someone without knowledge of monads could read it? It should hopefully be fairly obvious after reading the doc why the method is called chain.
Yeah, it's pretty easy - the chain() method lets you "chain" one promise into another - you start with a promise, take a function that returns a promise, and return a promise from that.
(Naming abstract operations is hard, but I think chain() is better than bind(). This is probably why Haskell spells it ">>=". ^_^)
2013/5/27 Andreas Rossberg <rossberg at google.com>
On 27 May 2013 15:30, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
What the discussion at last week's TC39 meeting clarified for me is the following:
- Promises are primarily a control-flow abstraction.
- Sometimes, they are also used as a data-abstraction (i.e. as a container in their own right, wrapping an arbitrary payload).
- All of the subtle problems discussed in these threads only start to arise when these two use cases of promises are being mixed, e.g. a promise-as-data-container being mistaken for a promise-as-control-abstraction. This rarely happens in application code, but may happen in generic library code.
Well, the gist of higher-order languages is that control flow abstractions are data. For example, that's the defining characteristics of first-class functions. And while most functions won't be used in a first-class manner (and most programmers probably don't think about them that way), the ability to do so gives great power -- as JavaScript demonstrates very well.
Futures/promises are an abstraction for first-class synchronisation. For the same reason you sometimes want to store or pass back & forth functions, you will sometimes want to store or pass promises. Not being able to combine those abstractions freely and transparently would arbitrarily limit their power, and practically demote promises to second-class status.
I agree with everything you said, but I fail to see why Q-style promises would become second-class. I have enjoyed writing and working with higher-order combinators like Q.all, which creates a promise for an array of promises. Your text above would seem to imply that writing or using such combinators would somehow be hampered by the recursive flattening, but I have never bumped into this issue. I think the reason is that when promises are used as data in combinators like Q.all, the composite abstraction as a whole remains a control-flow abstraction.
Are there a fixed number of use cases for promise subclasses? I've seen discussions about two possibilities referred to on this list, specifically a lazy-promise and a cancellable promise. I wonder if these two capabilities should instead be part of the Promise/Future API and not have subclasses promises. High-order operations like Q.all/Future.every will cause the subclass to be lost to the resulting operation.
I do agree with the earlier discussion that adding a cancel method to a promise can expose too much to the consumer, allowing multiple consumers of the same promise the ability to cancel the root. I have been experimenting 1 with supplying a cancellation signal from outside the promise that can be provided to the various API's (as an optional argument to the constructor and to then/done) to allow for cancellation but respect the separation of responsibilities between a promise and its creator. This makes cancellation optional, but fully baked into the API, as well as allowing the promise to fully cooperate with cancellation internally.
In a similar way we could enable lazy initialization of the promise via an argument to the constructor. Are there other use cases for promise subclasses?
Not subclassing promise doesn't prevent it from being a first-class object, just as how today you can't subclass Function, yet functions are first class.
Note: this is roughly based off of the DOM Futures spec with some non-spec additions, and performs auto-lift and single unwrap as discussed in an earlier thread.
My apologies, I've seen three use cases. The third use case being the ability to send progress notifications.
Sent from Windows Mail
From: Ron Buckton Sent: Monday, May 27, 2013 12:47 PM To: Andreas Rossberg, Tom Van Cutsem Cc: Mark S. Miller, Brendan Eich, es-discuss
Are there a fixed number of use cases for promise subclasses? I’ve seen discussions about two possibilities referred to on this list, specifically a lazy-promise and a cancellable promise. I wonder if these two capabilities should instead be part of the Promise/Future API and not have subclasses promises. High-order operations like Q.all/Future.every will cause the subclass to be lost to the resulting operation.
I do agree with the earlier discussion that adding a cancel method to a promise can expose too much to the consumer, allowing multiple consumers of the same promise the ability to cancel the root. I have been experimenting [1] with supplying a cancellation signal from outside the promise that can be provided to the various API’s (as an optional argument to the constructor and to then/done) to allow for cancellation but respect the separation of responsibilities between a promise and its creator. This makes cancellation optional, but fully baked into the API, as well as allowing the promise to fully cooperate with cancellation internally.
In a similar way we could enable lazy initialization of the promise via an argument to the constructor. Are there other use cases for promise subclasses?
Not subclassing promise doesn't prevent it from being a first-class object, just as how today you can’t subclass Function, yet functions are first class.
Ron
Note: this is roughly based off of the DOM Futures spec with some non-spec additions, and performs auto-lift and single unwrap as discussed in an earlier thread.
Sent from Windows Mail
From: Tom Van Cutsem Sent: Monday, May 27, 2013 8:09 AM To: Andreas Rossberg Cc: Mark S. Miller, Brendan Eich, es-discuss
2013/5/27 Andreas Rossberg <rossberg at google.com<mailto:rossberg at google.com>>
On 27 May 2013 15:30, Tom Van Cutsem <tomvc.be at gmail.com<mailto:tomvc.be at gmail.com>> wr
What the discussion at last week's TC39 meeting clarified for me is the following:
- Promises are primarily a control-flow abstraction.
- Sometimes, they are also used as a data-abstraction (i.e. as a container in their own right, wrapping an arbitrary payload).
- All of the subtle problems discussed in these threads only start to arise when these two use cases of promises are being mixed, e.g. a promise-as-data-container being mistaken for a promise-as-control-abstraction. This rarely happens in application code, but may happen in generic library code.
Well, the gist of higher-order languages is that control flow abstractions are data. For example, that's the defining characteristics of first-class functions. And while most functions won't be used in a first-class manner (and most programmers probably don't think about them that way), the ability to do so gives great power -- as JavaScript demonstrates very well.
Futures/promises are an abstraction for first-class synchronisation. For the same reason you sometimes want to store or pass back & forth functions, you will sometimes want to store or pass promises. Not being able to combine those abstractions freely and transparently would arbitrarily limit their power, and practically demote promises to second-class status.
I agree with everything you said, but I fail to see why Q-style promises would become second-class. I have enjoyed writing and working with higher-order combinators like Q.all, which creates a promise for an array of promises. Your text above would seem to imply that writing or using such combinators would somehow be hampered by the recursive flattening, but I have never bumped into this issue. I think the reason is that when promises are used as data in combinators like Q.all, the composite abstraction as a whole remains a control-flow abstraction.
On Tue, May 28, 2013 at 9:55 AM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:
On Mon, May 27, 2013 at 9:53 AM, Russell Leggett <russell.leggett at gmail.com> wrote:
On Mon, May 27, 2013 at 11:04 AM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Mon, May 27, 2013 at 7:29 AM, Russell Leggett <russell.leggett at gmail.com> wrote:
I'm just going to go ahead and play stupid here. Why is it called chain?
I agree one more method doesn't break anyone's brain, but I think the confusion comes when it is not clear what a method is for and when they
should use it. Can anyone just try to write a really quick API doc for chain so that someone without knowledge of monads could read it? It should hopefully be fairly obvious after reading the doc why the method is called chain.
Yeah, it's pretty easy - the chain() method lets you "chain" one promise into another - you start with a promise, take a function that returns a promise, and return a promise from that.
(Naming abstract operations is hard, but I think chain() is better than bind(). This is probably why Haskell spells it ">>=". ^_^)
Technically speaking, though, doesn't "then" also meet that definition (plus additional functionality)? I agree that naming abstract operations is hard, but this just screams to me of a method that would get improperly used/confused by developers. It would work like then in some cases, but break unexpectedly, etc. Brendan says no name mangling, and sure it probably shouldn't be too bad, but I think it should be on the burden of chain to distinguish itself from then, and not the other way around. ES is already filled with names like getOwnPropertyDescriptor. For the amount of use it will get, I'm not sure it's worth a shorter, more cryptic name.
"then" works equally well for promises, but I don't think it works for arbitrary containers - I think I'd understand "array.chain(cb)" better than "array.then(cb)".
I wan't suggesting using then for other containers or replacing chain with then, I was suggesting that the definition for chain on promises is a little ambiguous with the existence of 'then'. 'chain' chains promises together. "then" also chains promises together, but with some extra stuff. It puts developers in the position of wondering, "so when should I use 'then'? Why should I use it instead of 'chain'?" Given that 'then' is what they'll likely want in 99.9% (sorry for my made up statistic) of situations, I think that in the case of promises, it should be on the burden of the 'chain' method to distinguish itself from 'then'–to define itself in relation to 'then'. I think its rather similar to the discussion on arrow functions. => is what someone wants so much more often that -> was
left out. It was still possible, after all, its just normal functions, but it was decided that to aid comprehension, we would encourage the happy path.
I'm not arguing 'chain' be removed. I'm convinced at this point its worth including, I'm just debating the method name here. Sorry if it's just bikeshedding at this point, but on the face of it, the two methods seem hard to distinguish, and while 'chain' might be a better name for some hypothetical monadic style, why not leave it up to some library to give the method a facelift. The same way that the promise API is being kept light, and will likely still be wrapped by things like the Q library for additional functionality, I expect some monadic focused library will be used if using promises in the monadic style. Worst case scenario, you just add an alias on the Promise prototype.
On Tue, May 28, 2013 at 12:24 PM, Russell Leggett <russell.leggett at gmail.com
wrote:
On Tue, May 28, 2013 at 9:55 AM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:
On Mon, May 27, 2013 at 9:53 AM, Russell Leggett <russell.leggett at gmail.com> wrote:
On Mon, May 27, 2013 at 11:04 AM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Mon, May 27, 2013 at 7:29 AM, Russell Leggett <russell.leggett at gmail.com> wrote:
I'm just going to go ahead and play stupid here. Why is it called chain?
I agree one more method doesn't break anyone's brain, but I think the confusion comes when it is not clear what a method is for and when they
should use it. Can anyone just try to write a really quick API doc for
chain so that someone without knowledge of monads could read it? It should hopefully be fairly obvious after reading the doc why the method is called chain.
Yeah, it's pretty easy - the chain() method lets you "chain" one promise into another - you start with a promise, take a function that returns a promise, and return a promise from that.
(Naming abstract operations is hard, but I think chain() is better than bind(). This is probably why Haskell spells it ">>=". ^_^)
Technically speaking, though, doesn't "then" also meet that definition (plus additional functionality)? I agree that naming abstract operations is hard, but this just screams to me of a method that would get improperly used/confused by developers. It would work like then in some cases, but break unexpectedly, etc. Brendan says no name mangling, and sure it probably shouldn't be too bad, but I think it should be on the burden of chain to distinguish itself from then, and not the other way around. ES is already filled with names like getOwnPropertyDescriptor. For the amount of use it will get, I'm not sure it's worth a shorter, more cryptic name.
"then" works equally well for promises, but I don't think it works for arbitrary containers - I think I'd understand "array.chain(cb)" better than "array.then(cb)".
I wan't suggesting using then for other containers or replacing chain with then, I was suggesting that the definition for chain on promises is a little ambiguous with the existence of 'then'. 'chain' chains promises together. "then" also chains promises together, but with some extra stuff. It puts developers in the position of wondering, "so when should I use 'then'? Why should I use it instead of 'chain'?" Given that 'then' is what they'll likely want in 99.9% (sorry for my made up statistic) of situations, I think that in the case of promises, it should be on the burden of the 'chain' method to distinguish itself from 'then'–to define itself in relation to 'then'. I think its rather similar to the discussion on arrow functions. => is what someone wants so much more often that -> was left out. It was still possible, after all, its just normal functions, but it was decided that to aid comprehension, we would encourage the happy path.
I'm not arguing 'chain' be removed. I'm convinced at this point its worth including, I'm just debating the method name here. Sorry if it's just bikeshedding at this point, but on the face of it, the two methods seem hard to distinguish, and while 'chain' might be a better name for some hypothetical monadic style, why not leave it up to some library to give the method a facelift. The same way that the promise API is being kept light, and will likely still be wrapped by things like the Q library for additional functionality, I expect some monadic focused library will be used if using promises in the monadic style. Worst case scenario, you just add an alias on the Promise prototype.
I agree with Russell that chain
is a little ambiguous when juxtaposed
with then. Around the time this conversation first kicked up I'd proposed this API and referred to what Tab calls
chainas the "one-step resolver function". I still think
resolvewould be a pretty good, reasonably self-documenting name -- especially compared to
chain. You can describe
thenas a recursive
resolve` and the difference should be obvious.
I'm not arguing 'chain' be removed. I'm convinced at this point its worth
including, I'm just debating the method name here. Sorry if it's just bikeshedding at this point, but on the face of it, the two methods seem hard to distinguish, and while 'chain' might be a better name for some hypothetical monadic style, why not leave it up to some library to give the method a facelift. The same way that the promise API is being kept light, and will likely still be wrapped by things like the Q library for additional functionality, I expect some monadic focused library will be used if using promises in the monadic style. Worst case scenario, you just add an alias on the Promise prototype.
I agree with Russell that
chain
is a little ambiguous when juxtaposed withthen. Around the time this conversation first kicked up I'd proposed this API and referred to what Tab calls
chainas the "one-step resolver function". I still think
resolvewould be a pretty good, reasonably self-documenting name -- especially compared to
chain. You can describe
thenas a recursive
resolve` and the difference should be obvious.
Yes, I had suggested the name "resolveOnce" as I think it really helps describe what it does in relation to 'then'. As someone learning for the first time, I would likely read that and think, "Only once. That means something else resolves more than once." Reading the docs for 'then' and seeing the examples, would make it pretty clear to me that 'then' just works while resolveOnce is more of a low level method.
On 28 May 2013 20:04, Russell Leggett <russell.leggett at gmail.com> wrote:
Yes, I had suggested the name "resolveOnce" as I think it really helps describe what it does in relation to 'then'. As someone learning for the first time, I would likely read that and think, "Only once. That means something else resolves more than once." Reading the docs for 'then' and seeing the examples, would make it pretty clear to me that 'then' just works while resolveOnce is more of a low level method.
Neither is more low-level. Also, 'then' with recursive unwrapping does not work when you want to write generic abstractions properly -- which is why we need the alternative to start with.
Can we perhaps find a name for 'chain' that indicates that it is more modular?
The fact that we can make it monadic is just a bonus; any time you see "monad" just think "container++" - it's just a proven useful bit of structure on top which makes it easy to work with the values inside the container. (It's a bit more abstract than that, of course, but thinking in terms of containers works for many monads, and helps guide understanding toward the more abstract notion.)
You are aware of the over-simplification in that suggestion, but it can still be harmful to readers here (who may dismiss the simple examples and never get to the abstract notion). So, please pardon a little elaboration/clarification:
Monads are not about containers. Even functors (map-supporting things) are not about containers. That is confusing class-level constructs (Array<String>) with object-level constructs (["hi"]) and
misses out on some of the most interesting applications of monadic coding. The container analogy stops working when class-level constructs correspond to object-level control structures (eg, mapping over a promise means attaching a post-processor).
To give a use-case relevant to ES6 language design: generators were carefully designed to capture the flattest continuation that allows to stop and resume code in ES control structures without changing them. Monadic code captures even flatter continuations, and allows to define equivalent control structures in libraries, including generators and exceptions. In other words, good support for monadic coding allows to move language design decisions to libraries, and gives library authors expressive powers formerly reserved to language designers.
Monads first use case in programming languages was modular specification of language semantics for features like exceptions and non-determinism. That was then translated into modular program design for things like parsers, solution search, and embedded language interpreters. Some of the coding patterns go back to the 1980s, at least, but bringing them under the common head of monads and coding against a common monadic API started in the early 1990s.
This latter development allowed to work out commonalities between these coding patterns as well as sharing of code between control structure implementations: whether you need to implement a parser, embed a prolog interpreter, support exceptions, implement a type system, or a strategic rewrite library for code analysis and transformation passes - in the past, you started from scratch each time, these days, a good monad library gets you most of the way, provides valuable design guidelines, and pushes towards modular specifications and implementations.
In effect, monads and their cousins have started to give us similar structuring, sharing, and reuse tools for control structures as those we take for granted for data structures.
And because monads have helped us to see commonalities in different control-structure problems and their solutions, adding language support for monadic code supports all of these solutions at once. Instead of languages growing bigger with problem-specific constructs (generators, exceptions, promises, ...), languages can grow simpler again, off-loading specific solutions to library code while adding generic expressiveness to the language.
A lot of the early practical adoption of monads happened in a non-strict language, where data structures can stand in for control structures (eg, lazy lists for infinite iterators or promises). Also, monads where the class-level constructors corresponds to a simple object-level constructor are easier to present in monad tutorials. So it has become popular to present monads to containers with extras, but that is a very limited view. And it does not explain why monads have become so important to language designers and library authors alike.
Claus
On Thu, May 30, 2013 at 6:15 AM, Claus Reinke <claus.reinke at talk21.com> wrote:
The fact that we can make it monadic is just a bonus; any time you see "monad" just think "container++" - it's just a proven useful bit of structure on top which makes it easy to work with the values inside the container. (It's a bit more abstract than that, of course, but thinking in terms of containers works for many monads, and helps guide understanding toward the more abstract notion.)
You are aware of the over-simplification in that suggestion, but it can still be harmful to readers here (who may dismiss the simple examples and never get to the abstract notion). So, please pardon a little elaboration/clarification:
Believe me, it's not harmful. It's a useful, and for many people necessary, step in the process of understanding monads. Many of the common monads can be implemented and understood as containers, even if they really represent more abstract concepts. The more abstract monads are difficult to understand unless you've already passed through sufficient numbers of simpler examples, which often use the simple "container-like" monads to help comprehension.
I gained true understanding of monads relatively recently, so I still remember my learning process (which mostly involved reading Typeclassopedia over and over again until things clicked).
[+es-discuss]
I think the key exchange in these threads was
On Fri, May 3, 2013 at 4:17 PM, Jonas Sicking <jonas at sicking.cc> wrote:
On Sat, May 4, 2013 at 1:48 AM, Claus Reinke <claus.reinke at talk21.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 clarity I define the following APIs so I can define the three architectural choices in terms of the subset of these APIs they contain. Obviously, I do not intend to start a bikeshed yet on the particular names chosen for these operations. Let's stay focused on semantics, not terminology.
An upper case type variable, e.g. T, is fully parametric. It may be a promise or non-promise.
A lower case type variable, e.g. t, is constrained to be a non-promise. If you wish to think in conventional type terms, consider Any the top type immediately split into Promise and non-promise. Thus type parameter t is implicitly constrained to be a subtype of non-promise.
Ref<T>
is the union type ofT
andPromise<T>
.Q.fulfill(T) -> Promise<T> // the unconditional lift operator Q(Ref<t>) -> Promise<t> // the autolift operator p.map: Promise<T> -> (T -> U) -> Promise<U> p.flatMap: Promise<T> -> (T -> Promise<U>) -> Promise<U> // If the onSuccess function returns a non-promise, this would throw an Error, // so this type description remains accurate for the cases which succeed. p.then: Promise<T> -> (T -> Ref<u>) -> Promise<u> // Ignoring the onFailure callback
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
, andp.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?
This same abstraction in a monadic promise system would be written
class AsyncTable<T,U> { constructor() { this.m = Map<T,U>(); // encapsulation doesn't matter for this example } set(keyP :Promise<T>, val :U) { keyP.map(key => { this.m.set(key, val) }); } get(keyP :Promise<T>) :Promise<U> { return keyP.map(key => this.m.get(key)); } }
This works reliably.
This same abstraction in a Q-like promise system would be written
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)); } }
This works reliably. After erasure, note that this is exactly the same as the first example. The difference is that the code now correctly implements the annotated types representing the programmer's intention.
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.