- From: Tab Atkins Jr. <jackalmage@gmail.com>
- Date: Mon, 6 May 2013 10:43:09 -0700
- To: Jonas Sicking <jonas@sicking.cc>
- Cc: Domenic Denicola <domenic@domenicdenicola.com>, "public-script-coord@w3.org" <public-script-coord@w3.org>, "Mark S. Miller" <erights@google.com>
On Fri, May 3, 2013 at 4:17 PM, Jonas Sicking <jonas@sicking.cc> wrote: > On May 3, 2013 8:29 AM, "Tab Atkins Jr." <jackalmage@gmail.com> wrote: >> What I've been *trying* to do this entire time is find out *why* >> native nested promises are harmful. Every. Single. Person. who's >> tried to tell me that nested promises are harmful so far, has ended up >> realizing that they were talking about assimilation, or non-JS >> promises with weird language semantics, or programmer bugs that would >> benefit from causing an error, rather than having the >> excessive-wrapping bug ironed over. > > There are several problems with nested promises. > > First off they force people to answer the types of questions that > arise in this thread: How do I get to the value inside the doubly > nested promise? Which functions recursively assimilate and which > don't? How do I ensure that my nested promise doesn't get accidentally > unwrapped? Does that matter? > > I.e. permitting nested promises creates a more complex model and with > that you always get more confusion and more questions. This complexity doesn't arise automatically or accidentally. You only get it if your data model is complex, in which case having Futures map to that complexity better is a good thing. You brought up an example of such, where you have a retrieval-promise (from a db) for a compilation-promise (of an uncompiled function). These are meaningfully distinct sorts of promises, and while it *may* be useful to wait for both of them to complete before operating on the inner value (the compiled function), it's also reasonable to operate on the levels separately, waiting for the retrieval-promise to finish and then operating on the compilation-promise directly so you can pass it around. I keep asking for *examples* of complexity arising from nested promises, precisely to show that this complexity is never accidental (or if it is, it's a bug in your program that should be fixed rather than papered over, like returning a [[x]] rather than an [x]). > Second, and related, is the fact that trying to pass around a > promise-of-a-promise is very hard to make predictably work. There are > several functions in the current Future API which will either take a > Future or a value, and if it's a Future will unwrap it. If it weren't > for nested promises it would be safe to go through arbitrary number of > such APIs. I'm not sure what APIs you're referring to here. There is literally only one function which takes a Future or a value, and unwraps the Future: Future.resolve(). That's what it's *intended* for. Everything else either takes a value and acts on it generically (regardless of whether it's a Future or not), or takes only Futures. > It's likewise also possible to always call > Future.resolve(value) in order to create a promise representing a > value without having to worry about if that value is a promise or not. Future.accept() is the generic lift operator, putting a value into a promise regardless of what it is. Future.resolve() is for conditional lifting, when you want to raise things into a promise *if they aren't already one*, so you can operate on both types of values with a single code path. (This kind of thing often shows up for other container types as well, such as a conditional array wrapper, for making it convenient to deal with code that can accept either a value or an array of values.) > With nested promises you can always call Future.accept(myfuture) to > reliably create a nested promise. However once you pass that future > through some other code, you don't know how many times it going to > pass through APIs that may or may not unwrap layers of promises. For > example, you don't know how many times that value might pass as a > return value from a .then() callback, which would cause it to get > unwrapped. This is only an issue if you're writing .then() callbacks that don't use the value they get passed *at all*. If they did, they'd probably throw an error pretty quickly, as they'd be given a Future rather than whatever value type they expect to get. Most .then() code will operate on the value it receives, so this is not a concern to me. If you *do* have some generic Future-processing code that doesn't actually touch the passed value at all, it's trivial to get it right - just call Future.accept() on it before returning it. Code that doesn't do this is broken code, and I suspect it'll be mostly library code implementing Future combinators (like Future.all()). > Third, there's the question of what a nested promise actually means. > Normally a promise represents "a value after some time". But what does > a nested promise mean? "A value after a longer time" obviously isn't > correct since there are no time constraints associated with a promise. > "A promise after some time" also isn't really meaningful given that > that simply means "A value after some time after some time". Your example, which I discuss up above, shows what it really means. It's not "a value after some time", it's "a value after something else happens, after some time". That "something else happens" can be different for different promises, and this can be significant for your application. (Pretty much the *only* case where it's actually "after some time" is a delay operation, where the time itself is significant.) > This lack of a real mental meaning is what I think is the source of > the problems above. I.e. the fact that there is no good mental model > of what a nested promise actually means makes it hard to reason about > them and tell what is correct behavior and what isn't. The mental model is simple and sensical, as I show above. > This actually ties back to what the *real* question *should* be: > What's the use case? > > I.e. why should we support nested promises? It's a feature and we > don't add features without use cases. A big reason for this is that we > can't verify that a given solution works well unless we can test it > against use cases. So is it a problem that nested promises might get > arbitrarily unwrapped? Is it a problem that they don't get recursively > unwrapped when Future.resolve is used? Hard to say without knowing > what the use cases are. You've presented one use-case, albeit one whose particular details require several future steps. But the precise details of your presented use-case aren't that important; abstract just a tiny bit to cover an arbitrary database rather than IndexedDB specifically. It's not difficult to imagine other similar cases, where you have two unrelated types of asynchrony that you don't necessarily want to smash together. Finally, the monad abstraction has proven very useful in other languages. Most of those languages happen to share some features that JS doesn't have, generally surrounding type systems, but those features are only marginally relevant to the usefulness of the monad abstraction. It's not crazy to think that JS could benefit from this abstraction as well, and possibly even sprout dedicated syntax to handle them later - do-expressions are to the monad abstraction as list comprehensions are to the iterable abstraction. ~TJ
Received on Monday, 6 May 2013 17:43:56 UTC