Add Future[A] that helps with async code
See original GitHub issueI am lately writing a lot of asynchronous code (using asyncio mostly) and while async
/await
is fairly straight forward and nice to work with, it hinders writing modular code with small functions and reuse of said functions. This is imho due to two issues in particular:
- regular functions
def foo(...
and coroutinesasync def bar(...
have different calling semantics. For a regular function you just dofoo(...)
to get your result back while for coroutines you need to await the result, i.e. useawait bar(...)
- coroutines don’t compose well (without pouring
await
all over your code)
Let’s define a couple simple functions (both regular and coroutines) to illustrate these issues:
def double(x):
return 2*x
async def delay(x):
return x
async def double_async(x):
return 2*x
Cases:
-
Composing regular functions just for reference - easy:
double(double(1))
-
Composing regular and coroutine functions V1 - not so bad,:
await double_async(double(1))
-
Composing regular and coroutine functions V2 - awkward with the await thrown into the middle:
double(await double_async(1))
-
Composing two coroutine functions -
await
s galore :await double_async(await double_async(1))
To ease this pain I propose a (monad-like) container that implements map
and bind
so that functions can be chained nicely
class Promise:
def __init__(self, coro):
self._coro = coro
def map(self, f):
async def coro():
x = await self._coro
return f(x)
return Promise(coro())
def bind(self, f):
async def coro():
x = await self._coro
return await f(x)
return Promise(coro())
def __await__(self):
return self._coro.__await__()
Usage
- Wrap a coroutine (function defined with
async def
) to create an instance ofPromise
.p = Promise(delay(1)) await p # get the result of the wrapped coroutine
- Call a regular function on an instance of
Promise
await Promise(delay(1)).map(double)
- Call a coroutine on an instance of
Promise
await Promise(delay(1)).bind(double_async)
Extended example
Since the examples above don’t look too intimidating, behold:
await (
double_async(
double(
await double_async(
await delay(1)
)
)
)
)
vs.:
await (
Promise(delay(1))
.bind(double_async)
.map(double)
.bind(double_async)
)
Feedback greatly appreciated!
Issue Analytics
- State:
- Created 4 years ago
- Reactions:3
- Comments:31 (20 by maintainers)
Top GitHub Comments
Over the weekend I will probably have some spare time to work on this 😃 I also played around with lists of continuations (I implemented another monad
ListContinuation
that works over multiple continuations) because running multiple things at the same time is usually what async code is all about. I don’t know what your take is on more specialized monads, monad transfomers and extensible effects for nesting monads inside one another.After playing around
asyncio
andFuture
a bit, I would say that a correct way to handle this would be:async
functions we haveasync
keyword inwemake-python-styleguide
and banimport asyncio