How can async support dispatch between sync and async variants of the same code?

I don’t think that’s quite the same topic as this thread. That’s dispatch on the callee side, but on the caller side you’d have async functions only. So function color becomes a problem.

I’m not an expert in terms of what’s possible at the outer edge of language design, but it strikes me as problematic if x = y(); await x and await y() have different meanings. At the very least, it doesn’t match the current way that await composes with function calls.


It’s been a long time since I really thought about this problem – webargs has two separate call paths and that works fine – but I think it would be fun and interesting to play with a tool for doing this with codegen. Maybe if I get some of my other projects done this year… With the benefit of time and distance, that looks like the right solution because it’s the only way to change the name of the functions you’re calling. And that mapping of names can be parametrized and fine tuned. I’d implement narrow rules to do function name transforms. Maybe frame it a bit like cog. e.g. imagine this:

# async_foo.py

class AsyncFoo:
    async def bar(self) -> AsyncBaz:
        return await _async_quux()

# foo.py

# /// async2sync start
# convert("async_foo.py", "AsyncFoo")
# ///
class Foo:
    def bar(self) -> Baz:
        return _quux()
# /// async2sync end

Apologies if this is the wrong thread!

on the caller side you’d have async functions only. So function color becomes a problem.

I may be misreading you, or may have been unclear before, but the (offhand) pitch i was making was for having the behavior differ on the caller side so that a call without awaiting would be synchronous, and a call with await would be async.

so this:

if x = y(); await x and await y() have different meanings.

would not be a problem (unless the author chose to make it one) - in x = y(), x would not be awaitable, since it would run __call__. the purpose of the pitch was to make it so y() and await y() don’t have different meanings, but one can write an async-optimized version of the synchronous y without needing to have two methods with different names.

The motivation for me was a case very similar to the example in your comment:

from abc import ABC, abstractmethod
class Runner(ABC):
    @abstractmethod
    def process(self): ...

class SyncRunner(Runner):
    def process(self):
        # do something synchronously...
        return "x"

class AsyncRunner(Runner):
    async def process(self):
        # do something asynchronously...
        return "x"

where I want to have an ABC/protocol-like class with a common calling convention, but the async/sync implementations are substantially different enough that they make sense as being separate classes. I can mask all of that with existing languages except for the coloring problem, where a caller then always has to defensively write

if iscoroutine(runner.process):
    await runner.process()
else:
    runner.process()

if they want runner to be arbitrary across any subclass of Runner (there also isn’t a way to type hint this as far as i’m aware).

I could write

class Runner:
    def process(): ...

    async def aprocess(): ...

but then i have the problem of having that propagate through the rest of the class, where i also need to have async/sync versions of all the other private methods for want of a way to define doing “the same thing” async vs sync.

one could also imagine it being a decorator like this a la property setters

class Runner:
    def process(): ...

    @process.async
    async def process(): ...

that does something like this under the hood

#async decorator on the `process` method
class MethodType:
    def async(self, func):
        self.__acall__ = func

I don’t think it would be syntactically surprising except in the case that one wants to explicitly create the coroutine object without awaiting, but then that would just be, somewhat inelegantly:

coro = runner.process.__acall__()
await coro

but that would still be backwards compatible because there currently isn’t a mechanism to have dual-color methods, so existing async methods would have to opt-in to the new behavior, giving time for warnings and whatnot.

this can’t be done with code generation, I don’t think, which is why I thought it was worth thinking about as a language mechanism. The only way i found to actually implement this is to get the call stack and inspect the calling frame to see if it was awaiting the method, but that is janky and error prone.

I recently published a library that aims to solve this problem.

The idea is to write everything as coroutines, which internally branch into sync/async code paths only where necessary. Then, magic decorators take care of the rest.

class BaseClient:
    @zyncio.zmethod
    async def fetch(self, url: str) -> str:
        if zyncio.is_sync(self):
            # do sync fetch
        else:
            # do async fetch

    @zyncio.zmethod
    async def get_status(self) -> str:
        return await self.fetch.z('/api/status')


class Client(BaseClient, zyncio.SyncMixin):
    pass

class AsyncClient(BaseClient, zyncio.AsyncMixin):
    pass


print(Client().get_status())

print(await AsyncClient().get_status())

The sync version runs the coroutine directly with a single call to .send(None), which works as long as it doesn’t encounter any Futures along the way.

Everything is fully typed, so you get full IDE support.

2 Likes