Chaining asyncio futures?

Other async frameworks have constructs similar to asyncio.Future, e.g. JavaScript has Promise[1] and Dart has a Completer/Future pair[2][3]. (For the purpose of this e-mail, I'll call the generic concept a "Future".) JavaScript and Dart allow futures to be chained, such that one Future's callback is invoked only when the other Future completes. For example, consider an API that fetches a string from a URL and returns it, but also has an MRU cache. This API returns a Future because the caller does not know in advance if the string to be retrieved is cached or not. If the string is in cache, then the Future can complete immediately, but if the string it not in cache, then the string needs to be fetched over the network, perhaps by calling some API that returns a Future string. In this case, it would be nice to return a Future string that is chained to the HTTP request's future string.[4] I tried doing this with asyncio.Future: import asyncio f1 = asyncio.Future() f2 = asyncio.Future() f2.add_done_callback(lambda f: print('done: {}'.format(f.result()))) f2.set_result(f1) f1.set_result('hello world!') loop = asyncio.get_event_loop() loop.run_until_complete(f2) Output: done: <Future finished result='hello world!'> This does not make f2 dependent on f1; it immediately sets f2's result to repr(f1). It's easy to modify the example so that f1's callback explicitly sets f2's result. Abstracting a bit, we can write a chain_futures() function. def chain_futures(a, b): a.add_done_callback(lambda _: b.set_result(a.result())) f3 = asyncio.Future() f4 = asyncio.Future() f4.add_done_callback(lambda f: print('done: {}'.format(f.result()))) chain_futures(f3, f4) f3.set_result('hello world!') loop.run_until_complete(f4) Output: done: hello world! Wouldn't this be a nice feature in asyncio.Future? E.g. instead of `chain_futures(f3, f4)`, either `f3.set_result(f4)` or something more explicit like `f3.set_result_from(f4)`. [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Obj... [2] https://api.dartlang.org/stable/1.17.1/dart-async/Completer-class.html [3] https://api.dartlang.org/stable/1.17.1/dart-async/Future-class.html [4] Why not return the HTTP request Future directly? That feels hacky -- it's a leaky abstraction.

For a reference point: Twisted’s Deferreds have this already, written almost exactly in the form you’re considering it: https://twistedmatrix.com/documents/current/core/howto/defer.html#chaining-d... <https://twistedmatrix.com/documents/current/core/howto/defer.html#chaining-d...>. The biggest difference for Twisted is that it also hooks up the errbacks, which is a semantic that doesn’t make sense for asyncio Futures. While discussing this, the natural way to write this kind of code in Twisted is actually to return Deferreds from a callback (the asyncio equivalent would be to return a Future from a done callback). In Twisted, if a callback returns a Deferred the next callback in the chain is not called until the Deferred resolves, at which point it is called with the result of that Deferred. Essentially, then, the program shown above returns only a single Deferred with a long series of callbacks that transform the data, some of which operate asynchronously. As best as I can work out, the only reason that asyncio Future’s don’t behave this way is that asyncio strongly emphasises the use of coroutines to program in this manner. Put another way, the asyncio-native way to write that code is not to chain Futures, but instead to write a coroutine that manages your flow control for you: async def do_work(): result = await check_cache(url) if not result: result = await do_web_request(url) print(‘done: {}’.format(f.result())) loop.run_until_complete(do_work()) This is not a reason in and of itself not to have chain_futures as a thing that exists. However, I think it may be the case that the asyncio core developers aren’t hugely interested in it. I’m now reading a bit into the mindset of Guido so I’m sure he could step in and correct me, but I seem to recall that asyncio’s Futures were deliberately designed to have less of the complexity of, say, Twisted’s Deferreds, in part because coroutines were intended to supersede some of the more complex functionality of Deferreds. Cory

For a reference point: Twisted’s Deferreds have this already, written almost exactly in the form you’re considering it: https://twistedmatrix.com/documents/current/core/howto/defer.html#chaining-d... <https://twistedmatrix.com/documents/current/core/howto/defer.html#chaining-d...>. The biggest difference for Twisted is that it also hooks up the errbacks, which is a semantic that doesn’t make sense for asyncio Futures. While discussing this, the natural way to write this kind of code in Twisted is actually to return Deferreds from a callback (the asyncio equivalent would be to return a Future from a done callback). In Twisted, if a callback returns a Deferred the next callback in the chain is not called until the Deferred resolves, at which point it is called with the result of that Deferred. Essentially, then, the program shown above returns only a single Deferred with a long series of callbacks that transform the data, some of which operate asynchronously. As best as I can work out, the only reason that asyncio Future’s don’t behave this way is that asyncio strongly emphasises the use of coroutines to program in this manner. Put another way, the asyncio-native way to write that code is not to chain Futures, but instead to write a coroutine that manages your flow control for you: async def do_work(): result = await check_cache(url) if not result: result = await do_web_request(url) print(‘done: {}’.format(f.result())) loop.run_until_complete(do_work()) This is not a reason in and of itself not to have chain_futures as a thing that exists. However, I think it may be the case that the asyncio core developers aren’t hugely interested in it. I’m now reading a bit into the mindset of Guido so I’m sure he could step in and correct me, but I seem to recall that asyncio’s Futures were deliberately designed to have less of the complexity of, say, Twisted’s Deferreds, in part because coroutines were intended to supersede some of the more complex functionality of Deferreds. Cory
participants (2)
-
Cory Benfield
-
Mark E. Haase