A Challenge Problem for Promise Designers (was: Re: Futures)

Dean Tribble tribble at e-dean.com
Thu Apr 25 16:30:24 PDT 2013


I've built multiple large systems using promises. A fundamental distinction
that must be clear to the client of a function is whether the function
"goes async":  does it return a result that can be used synchronously or
will the result only be available in a later turn. The .Net async libraries
require the async keyword precisely to surface that in the signature of the
function; i.e., it is a breaking change to a function to go from returning
a ground result vs. a promise for a result.  The same is basically isn't
true for returning a promise that will only be resolved after several turns.

For example: I have a helper function to get the size of contents at the
other end of a URL. Since that requires IO, it must return a Promise<int>.

size: (url) => {
    return url.read().then(contents => contents.length)
}

This is obviously an expensive way to do it, and later when I get wired
into some nice web caching abstraction, I discover that the cache has a
similar operation, but with a smarter implementation; e.g., that can get
the answer back by looking at content-length in the header or file length
in the cache. That operation of course may require IO so it returns a
promise as well. Should the client type be different just because the
implementation uses any of several perfectly reasonable approaches for the
implementation.

size: (url) => {
    return _cacheService.getLength(url)
}

If in order to not change the signature, I have to "then" the result, it
leads to

size: (url) => {
    return _cacheService.getLength(url).then(length => length)
}

This just adds allocation and scheduling overhead for the useless then
block, precludes (huge) tail return optimization, and clutters the code.
This also leads to a depth of nesting types which is comparable to the
function nesting depth (i.e., if x calls y calls z do I have
promise<promise<promise<Z>>>?), which is overwhelming both to the type
checkers and to the programmers trying to reason about the code. the client
invoked an operation that will eventually produce the integer they need.

There is also a relation between flattening and error propagation: consider
that returning a broken promise is analogous to throwing an exception in
languages with exceptions. In the above code, if the cache service fails
(e..g, the URL is bogus), the result from the cache service will
(eventually) be a rejected promise. Should the answer from the size
operation be a fulfilled promise for a failed result? That would extremely
painful in practice.  Adding a layer of promise at each level is equivalent
in sequential to requiring that every call site catch exceptions at that
site (and perhaps deliberately propagate them).  While various systems have
attempted that, they generally have failed the usability test. It certainly
seems not well-suited to the JS environment.

There are a few cases that may require "promise<promise<T>>". Most can be
more clearly expresses with an intermediate type. For example, in an
enterprise security management system, the service manager returned a
promise for a (remote) authorization service, but the authorization service
might have been broken. Instead of returning a
Promise<Promise<AuthorizationService>>, it returned
Promise<AuthorizationConnection> where AuthorizationConnection had a member
"service" that returned a Promise<AuthorizationService>.  When you deal
with higher level abstractions in a parameterized type system like C#s,
however, you may end up with APIs that want to work across any T, including
promises.  If the abstractions internally use promises, then they may well
end up with Promise<T> where T : Promise<U> or some such.  Those are very
rare in practice, and can typically make use of operators (e.g., like Q) to
limit their type nesting depth.

On Thu, Apr 25, 2013 at 3:31 PM, Domenic Denicola <
domenic at domenicdenicola.com> wrote:

>   Can you point to any code in wide use that makes use of this "thenables
> = monads" idea you seem to be implicitly assuming? Perhaps some of this
> "generic thenable library code"? I have never seen such code, whereas the
> use of "thenable" to mean "object with a then method, which we will try to
> treat as a promise" as in Promises/A+ seems widely deployed throughout
> libraries that are used by thousands of people judging by GitHub stars
> alone.
>
> Thus I would say it's not promise libraries that are "harming the thenable
> operations," but perhaps some minority libraries who have misinterpreted
> what it means to be a thenable.
>  ------------------------------
> From: Claus Reinke <claus.reinke at talk21.com>
> Sent: 4/25/2013 18:21
> To: Mark Miller <erights at gmail.com>; David Bruant <bruant.d at gmail.com>
> Cc: Mark S. Miller <erights at google.com>; es-discuss<es-discuss at mozilla.org>
> Subject: Re: A Challenge Problem for Promise Designers (was: Re: Futures)
>
>   I'm still wading through the various issue tracker threads, but only two
> concrete rationales for flattening nested Promises have emerged so far:
>
> 1 "library author doesn't want nested Promises."
> 2 crossing Promise library boundaries can create unwanted nesting
>
> There is little to be said about 1, only that those library authors still
> have a choice: add a separate recursive flattening operation and keep
> the thenable operations unharmed, or give up on Promises being
> thenables in the monad-inspired JS patterns sense (and hence give
> up on profiting from generic thenable library code).
>
> The second point is somewhat more interesting, as it stems from yet
> another convenience-driven thenable deviation: if a then-callback does
> not return a Promise, its result is implicitly lifted into a Promise; the
> unwanted nesting apparently comes from different libs not recognizing
> each others promises, mistaking foreign promises for values, and lifting
> them into their own promises. Recursive flattening (assimilation) is
> then intended as a countermeasure to recursive lifting of foreign
> promises.
>
> It will come as no surprise that I think implicit lifting is just as
> mistaken
> and recursive flattening;-) Both should be moved to explicit convenience
> methods, leaving the generic 'then'/'of' interface with the properties
> needed for generic thenable library code.
>
> Claus
>
> >> I think we see a correlation -- not a 1.0 correlation, but something.
> Those
> >> who've actually used promise libraries with this flattening property
> find
> >> it pleasant. Those who come from either a statically typed or monadic
> >> perspective, or have had no experience with flattening promises,
> generally
> >> think they shouldn't flatten.
> >
> > I think the dispute could be settled easily:
> >
> > - flattening 'then' is a convenience
> > - non-flattening 'then' is necessary for promises being thenables
> >    (in the monad-inspired JS patterns sense)
> >
> > Why not have both? Non-flattening 'then' for generic thenable
> > coding, and a convenience method 'then_' for 'then'+flattening.
> >
> > That way, coders can document whether they expect convenience
> > or standard thenable behavior. And we can have convenience for
> > Promise coding without ruining Promises for more general thenable
> > coding patterns.
> >
> > Claus
> >
> > _______________________________________________
> > es-discuss mailing list
> > es-discuss at mozilla.org
> > https://mail.mozilla.org/listinfo/es-discuss
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss
>
>
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20130425/66903435/attachment.html>


More information about the es-discuss mailing list