Like in Lua, async/await should be the default

I’ve recently noticed that the Lua programming language has this cool feature that allows you to write normal (imperative) lua code but have some functions be able to yield their execution and run asynchronously. This means that you can’t tell between synchronous functions or asynchronous functions just by looking at the code. This strikes to me as very useful as the average programmer doesn’t really need to care about this. He mostly cares about the business logic, simplicity and the readability of his code.

So instead of having:

import asyncio
async def bar1():
await asyncio.sleep(1)
return "good result"

async def bar2(x):
await asyncio.sleep(1)
return x.replace("good", "better")

async def bar3(x):
await asyncio.sleep(1)
return x.replace("better", "best")

async def foo():
return await bar3(await bar2(await bar1()))

# returns in 3 seconds
loop = asyncio.get_event_loop()
print(loop.run_until_complete(foo()))

We can simply have:

import time
def bar1():
time.sleep(1)
return "good result"

def bar2(x):
time.sleep(1)
return x.replace("good", "better")

def bar3(x):
time.sleep(1)
return x.replace("better", "best")

def foo():
return bar3(bar2(bar1()))

# returns in 3 seconds
print(foo())

Much more readable, and requires much less thought. It should even make pure Python libraries that were written in a synchronous manner to work asynchronously out of the box.

So — one might ask, what if we want to launch all tasks together and wait for all of them to complete so they would complete faster, which is why we did all of this in the first place?

import asyncio
async def bar1():
await asyncio.sleep(1)
return "good result"

async def bar2():
await asyncio.sleep(1)
return "better result"

async def bar3():
await asyncio.sleep(1)
return "best result"

async def foo():
return await asyncio.gather(bar1(), bar2(), bar3())

# returns in 1 second
loop = asyncio.get_event_loop()
loop.run_until_complete(foo())

In this case, the solution should be to explicitly call out the invocations as non-blocking, which we should not await for. For example:

import time
def bar1():
time.sleep(1)
return "good result"

def bar2():
time.sleep(1)
return "better result"

def bar3():
time.sleep(1)
return "best result"

def foo_serial():
return [bar1(), bar2(), bar3()]

def foo_parallel():
return asyncio.gather(nowait bar1(), nowait bar2(), nowait bar3())

# returns in 3 seconds
foo_serial()

# returns in 1 second
foo_parallel()

Isn’t that better?

Like what you read? Give Ron Reiter a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.