jfhbrook / pyee

A rough port of Node.js's EventEmitter to Python with a few tricks of its own
https://github.com/jfhbrook/public
MIT License
362 stars 37 forks source link

Support for asynchronous methods. #24

Closed jAlpedrinha closed 8 years ago

jAlpedrinha commented 8 years ago

Have you considered supporting asynchronous functions as handlers or even creating an asynchronous emit function ?

This would only be supported in 3.4+, but in my use case is a must. If you're interested I could elaborate a bit more on the requirements.

I'd be available to work with you on this, never breaking backwards compatibility but making such methods available for 3.4+ adopters.

WDYT?

jfhbrook commented 8 years ago

Events are generally fire-and-forget, so it doesn't really make sense to have any sort of asynchronous-specific support. A function is a function. You can call it whenever you want, and deal with the fallout how you will. Moreover, this is a pretty cut-and-dry implementation of nodejs-style EEs, and I don't really want to stray too far from that.

In other words, I'm not really interested in supporting any features beyond what's already implemented, but I can still talk use case with you, if you think I might have a suggestion in terms of alternate patterns.

jfhbrook commented 8 years ago

(There's also the odd chance that I'm missing something due to unfamiliarity with how python implements futures/etc)

jAlpedrinha commented 8 years ago

Not sure with what you mean with fire-and-forget, since EventEmitter has synchronous behaviour. emit only returns execution when all handlers are called instead of using something like yield from handler which would allow to really fire the event and forget what will happen next, without having to wait for the handler to end execution.

I understand that this has a natural solution in nodejs by using setImmediate, which isn't as natural in python, since there isn't by default an event loop.

If you feel like sticking to EventEmitter like it exists in nodejs, there's no problem.

jfhbrook commented 8 years ago

I don't follow. What I mean is that no result from any actions your EE takes are retained, so the function handler can easily kick off an asynchronous action no problem. Like in javascript you would just do

ee.on('event', (data) => {
  // This schedules an async action, but the actual meat of this function beyond validation
  // happens on a subsequent tick, and the EE's handler doesn't do anything with the
  // result (this is what I mean by "fire and forget")
  doSomeAsyncThing((err) => {
    if (err) throw err; // Whatever
    console.log('did the thing');
  });
});

and you're off to the races! I have to assume that in python you would do something similar. Looking at [https://docs.python.org/3/library/asyncio-task.html#example-hello-world-coroutine](python 3's docs for asyncio) I assume you'd just make some calls to loop with some coroutines.

But, out of curiosity, what would an example of your proposal Look Like? Maybe seeing a code sample will make this make more sense.

jAlpedrinha commented 8 years ago

I'll provide you an example and explain the nature of async and await.

import asyncio
import pyee

ee = pyee.EventEmitter()

async def sleep(seconds):
  print('abc')
  await asyncio.sleep(seconds)
  print('dow')

ee.on('test', sleep)

async def start():
  print("I will emit")
  ee.emit('test', 2)
  print("I have emitted")
  await asyncio.sleep(10)

loop = asyncio.get_event_loop()
loop.set_debug(True)

loop.run_until_complete(start())
print ('done')

If you run this snippet you can see that you'll receive an error because I've set_debug to True on the event loop, if you disable you'll just get the same message as a warning.

The reason for this is that an async function is a coroutine which returns the execution to its caller after it is invoked returning a future. If this future is not awaited (or yielded) the code will simply not run.

This doesn't need to occur imediately, you can retrieve the future and wait for it later on, even though the stream of execution will temporarily stop (which seems like synchronous code) the reality is that if there is any other stream of execution running concurrently it will be able to continue while this is waiting.

This is highly similar to the ES proposal here

My proposal to change this would be to have an emit_async that would be a coroutine that would await handlers instead of just invoking them, that could itself be awaited. There would also be some changes on the once wrapper that would still work as it does now, but would behave differently when the handler is a coroutine.

I understand if you don't want to pursue this path, and in that case I will be more than happy to implement a version of this code. I'm just sharing this because I really love the observer pattern, and this was one of the cleanest implementations of it that I've found. But since I'm embracing the asynchronous side of python, which makes sense but breaks with some of the current establishment, I'd like to contribute with your effort.

jfhbrook commented 8 years ago

So what you're saying is that we'd "like" to see the following (because async/await -> futures is the async io abstraction in python)

I will emit
abc
I have emitted
dow
done

but right now it prints

I will emit
abc
dow
I have emitted
done

?

Is there a way to switch on whether something is a couroutine/future-generating-function rather than a Boring Function, so that we don't need an alternate implementation? Is there a way to explicitly make it so you don't have to await (or maybe even can't await) the ee call (it sounds like you might have to in this proposal)?

You made comparisons to JavaScript's async/await, which I believe behind the scenes are just packing/unpacking promises (and yield/generators with iterators). Arguably, shouldn't your function handlers just be more explicit about this? I haven't heard of anyone making similar proposals for node, and promisified-by-default core modules are certainly being discussed.

jfhbrook commented 8 years ago

This looks relevant http://stackoverflow.com/questions/37278647/fire-and-forget-python-async-await

jfhbrook commented 8 years ago

I want to use https://docs.python.org/3.5/library/types.html#types.CoroutineType to switch on whether @ee.on is decorating a coroutine or a regular function (note: do I want to switch on generators for completeness?), and then use https://docs.python.org/3/library/asyncio-task.html#asyncio.ensure_future (or possibly an overridable version of this?) with a possible override to schedule coroutines to run.

Something like,

@ee.on('fire')
async def fire_and_forget():
    await async_things()

// from somewhere
@ee.emit('fire')

loop.close()

I think? Still learning how the event loop works.