Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

events #31

Open
faassen opened this issue May 30, 2016 · 13 comments
Open

events #31

faassen opened this issue May 30, 2016 · 13 comments

Comments

@faassen
Copy link
Member

faassen commented May 30, 2016

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
Copy link
Member Author

faassen commented May 30, 2016

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

@henri-hulski
Copy link
Member

henri-hulski commented Jun 2, 2016

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
Copy link
Member

henri-hulski commented Jun 2, 2016

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
Copy link
Member

henri-hulski commented Jun 2, 2016

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
Copy link
Member Author

faassen commented Sep 7, 2016

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
Copy link

kagesenshi commented Aug 16, 2017

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
Copy link

kagesenshi commented Aug 16, 2017

and if we can have #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
Copy link
Member Author

faassen commented Aug 17, 2017

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

  • by making signal come second in the predicates I think it binds less strongly than the model. I think it makes sense to make it come first.

  • The other question is whether we need a signal parameter at all: instead the actual dispatch method can serve the purpose of distinguishing between two kinds of signals. Perhaps it's enough to explain to people how to make new dispatch methods?

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
Copy link

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
Copy link

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

@goschtl
Copy link

goschtl commented Oct 29, 2020

Any update on this?

@faassen
Copy link
Member Author

faassen commented Oct 30, 2020

I think we could go two ways:

  • integrate this into Reg

  • capture it into a separate library

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

@goschtl
Copy link

goschtl commented Nov 5, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants