morepath / reg

Clever dispatch
https://reg.readthedocs.org
BSD 3-Clause "New" or "Revised" License
76 stars 12 forks source link

events #31

Open faassen opened 8 years ago

faassen commented 8 years ago

Some way to build events on top of Reg. We should be able to select all things that match a certain set of predicates, including things that match less strongly. Then fire off events at it.

faassen commented 8 years ago

@henri-hulski mentions the signals API in Cerebral (JS) is interesting.

henri-hulski commented 8 years ago

A nice description, how the signals in Cerebral works is in the README of redux-action-tree, which is a port of Cerebral signals to Redux.

It starts with a short description of its concept:

What are these Cerebral signals conceptually?

With Redux you typically think of actions and action creators. Action creators are like commands, they tell your app what to do. For example when your application mounts you would trigger an action creator saying: "getInitialData". With signals you do not let your UI (or other events) command your application logic, they only tell your application what happened. This also aligns with the concept of keeping your UI as dumb as possible.

A good analogy for signals is how your body works. If you burn your finger the finger does not command your arm to pull away. Your finger just sends a signal to your brain about it being burned and the brain changes your "state of mind", which the arm will react to. With this analogy you would not name your signal "getInitialData", but "appMounted", because that is what happened. Your signal then defines what is actually going to happen... which in this case is getting the initial data.

A signal uses an action-tree tree to define its behaviour. Think of this as a behaviour tree, like in games. It makes you able to declaratively describe what is going to happen in your app when a signal triggers.

For sure for Morepath this can only be an inspiration, as we've to solve slightly different issues.

henri-hulski commented 8 years ago

The Cerebral signals implementation was extracted to action-tree, which is written in typescript, to allow other project to use it as well. It's actually used by redux-action-tree.

Overview

Core idea of this project is to expose the bare minimum implementation of signal execution in Cerebral and let other projects take advantage of it as well. Signals is a great concept to express flow of actions executed as a response to interaction events or other events in your application. It was initially introduced by the cerebral project with its declarative way of using function references, arrays and objects to define execution flow. Starting as an experiment, cerebral proved itself to be a solid solution to build real life web applications.

henri-hulski commented 8 years ago

A short indroduction from http://www.cerebraljs.com/signals which could also be relevant to Morepath:

Signals

The way you think of signals is that something happened in your application. Either in your UI, a router, maybe a websocket connection etc. So the name of a signal should define what happened: appMounted, inputChanged, formSubmitted. The functions in a signal are called actions. They are named by their purpose, like setInputValue, postForm etc. This setup makes it very easy for you to read and understand the flow of the application.

faassen commented 8 years ago

While with dispatch a single function ends up getting called, with events there is the implication of one to many behavior: to a single event you could have multiple subscribers. There's also the implication of inheritance. If B gets added and B subclasses from A, a subscriber that listens for add events of A sshould also get notified. If something gets added to folder D and it subclasses from C, a subscriber that listens for add events to folder C should also get notified. So it would appear all() is in play.

@taschini I'm curious whether you have ideas on this.

kagesenshi commented 7 years ago

seems like can be implemented by simply using a register method on PredicateRegistry that does not check for duplicates. following monkeypatch added events in form of .publish() and subscribe() method to Dispatch


import reg
from reg.dispatch import Dispatch, validate_signature
from reg.context import DispatchMethod
from reg.predicate import PredicateRegistry
from functools import partial

def _dispatch_subscribe(self, func=None, **key_dict):
    if func is None:
        return partial(self.subscribe, **key_dict)
    validate_signature(func, self.wrapped_func)
    predicate_key = self.registry.key_dict_to_predicate_key(key_dict)
    self.registry.subscribe(predicate_key, func)
    return func

def _dispatch_publish(self, *args, **kwargs):
    subscribers = self.by_args(*args, **kwargs).all_matches
    return list([sub(*args, **kwargs) for sub in subscribers])

def _dispatchmethod_publish(self, app, *args, **kwargs):
    subscribers = self.by_args(*args, **kwargs).all_matches
    return list([sub(app, *args, **kwargs) for sub in subscribers])

def _registry_subscribe(self, key, value):
    for index, key_item in zip(self.indexes, key):
        index.setdefault(key_item, set()).add(value)

if not getattr(PredicateRegistry, '__pubsub_patched', False):
    PredicateRegistry.subscribe = _registry_subscribe
    PredicateRegistry.__pubsub_patched = True

if not getattr(Dispatch, '__pubsub_patched', False):
    Dispatch.subscribe = _dispatch_subscribe
    Dispatch.publish = _dispatch_publish
    Dispatch.__pubsub_patched = True

if not getattr(DispatchMethod, '__pubsub_dispatchmethod_patched', False):
    DispatchMethod.publish = _dispatchmethod_publish
    DispatchMethod.__pubsub_dispatchmethod_patched = True

and tests

import reg

def test_event():

    class Model(object):
        pass

    class SubModel(Model):
        pass

    @reg.dispatch(reg.match_instance('model'),
                  reg.match_key('signal', lambda model, signal: signal))
    def event(model, signal):
        raise NotImplementedError

    @event.subscribe(model=Model, signal='event')
    def one(model, signal):
        return 1

    @event.subscribe(model=Model, signal='event')
    def two(model, signal):
        return 2

    @event.subscribe(model=SubModel, signal='event')
    def three(model, signal):
        return 3

    mobj = Model()
    smobj = SubModel()
    assert list(sorted(event.publish(model=mobj, signal='event'))) == [1, 2]
    assert list(
        sorted(event.publish(model=smobj, signal='event'))) == [1, 2, 3]

def test_event_dispatchmethod():

    class App(object):

        @reg.dispatch_method(reg.match_instance('model'),
                             reg.match_key('signal', lambda self, model, signal: signal))
        def event(self, model, signal):
            raise NotImplementedError

    class Model(object):
        pass

    class SubModel(Model):
        pass

    @App.event.subscribe(model=Model, signal='event')
    def one(app, model, signal):
        return 1

    @App.event.subscribe(model=Model, signal='event')
    def two(app, model, signal):
        return 2

    @App.event.subscribe(model=SubModel, signal='event')
    def three(app, model, signal):
        return 3

    app = App()
    mobj = Model()
    smobj = SubModel()
    assert list(sorted(
        app.event.publish(app, model=mobj, signal='event'))) == [1, 2]
    assert list(sorted(
        app.event.publish(app, model=smobj, signal='event'))) == [1, 2, 3]
kagesenshi commented 7 years ago

and if we can have https://github.com/morepath/reg/issues/21 fixed, then there's no need to have .publish() as currently the dispatch function only executes the first function that match . Also, not quite sure on how to make app always passed to the function during publish when implemented as dispatch method

faassen commented 7 years ago

Two comments on the use of the 'signal' predicate:

If we remove the restriction from the registry so that we can register multiple components for the same key, I think it becomes unpredictable which component ends up being called in the normal function call scenario. So I'm -1 on that for our plain dispatch function.

I don't think we have a use case where we want a dispatch function to sometimes behave like a normal function and sometimes to call all the things we've registered on it. Those are separate use cases.

So I think it would make sense to define a new kind of dispatch function that dispatches to all matches instead of to only the first one. Its API is the same as for dispatch functions: we could use .register to register subscribers and you call it to call all matches. We could layer this over the dispatch function we already have, and we register an object that we can register components with and when called calls all those components.

    # to have a point to register
    @reg.dispatch_all(reg.match_instance('model'))
    def my_event(model):
        pass

   # to register
    @my_event.register(model=Model')
    def one(model, signal):
        return 1

   # to call
   my_event(mobj)

We also need to figure out what the default implementation does in this case. When should it get called? You've made it raise NotImplementedError but I think that's wrong. I suspect we should want it to be called always, even if no other matches exist, as the last match.

Enabling this would require quite a bit of refactoring to avoid code duplication, especially to also have a dispatch_all_method.

Incidentally we have an API "all_matches" on the LookupEntry that already returns all matches. (and matches that returns an iterator).

kagesenshi commented 7 years ago

On signal predicate, that is just an example implementation, so its only in the tests :). And yeah, using the function itself to distinguish which signal is which would be more consistent with the rest of reg API. Currently i'm using match_key for signal in my project to avoid defining dectate directive and dispatch method for each and every signal type i want to dispatch (which is quite a bit).

on having separate reg dispatch function, +1 on that idea, that will help cleanly separate normal dispatch and subscriber dispatch.

on default implementation, executing the default implementation as final function make sense to me.

kagesenshi commented 5 years ago

any idea if this is going to be implemented? .. i'm heavily using this now

goschtl commented 3 years ago

Any update on this?

faassen commented 3 years ago

I think we could go two ways:

If the latter is possible, then I'd prefer that. We might need to expose more APIs in Reg to do so.

goschtl commented 3 years ago

Hey,

thanks for your answer. Yes if i find some time i try to sketch out a 3'rd party lib for it.

Thx Christian