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

Tab Atkins Jr. jackalmage at gmail.com
Thu Apr 25 17:08:51 PDT 2013


On Thu, Apr 25, 2013 at 4:30 PM, Dean Tribble <tribble at e-dean.com> wrote:
> 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.

I don't understand this example.  In the last one, if the return value
of _cacheService.getLength(url) is a future already, why do you need
to call .then() on it?  Are you unsure of whether getLength() returns
a Future<length> or a Future<Future<length>>?  If so, getLength() is
terribly broken, and should be flattening stuff by itself to return a
consistent type.  We don't need to engineer around that kind of
application design mistake at the language level.

> 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.

I don't understand this either, probably because I don't understand
the reason for the .then() in the earlier example.  If
cacheService.getLength() returns a future, then you don't need to do
anything special in the size() function - just return the future that
it returns.  It sounds like you're nesting values in futures for the
hell of it, which of course is problematic.  Hiding the application's
mistakes by auto-flattening isn't a good idea.

If you don't know whether a given function will return a bare value or
a future, but you need to return a future, there's always
Future.resolve(), which has the same semantics "unwrap 0 or 1 layers"
semantics as Future#then.

I can't quite understand the point of the examples given here, so I
may be misinterpreting them uncharitably.  Could you elaborate on
them, particularly on the cacheService example?  What does
cacheService.getLength() return, and what does size() need to return?

~TJ


More information about the es-discuss mailing list