Proposed alternative callback API

The current callback API forces one to register callbacks on their senders. As a consequence, and also because callbacks rely on side-effects, intended receivers are not always obvious, particularly when the callback code is long. I propose an alternative, receiver-centric API. Here's a possible implementation for illustration:

<pre>
from bokeh.layouts import column
from bokeh.core.has_props import HasProps
from bokeh.models import ColumnDataSource, Slider
from bokeh.plotting import curdoc, figure
from inspect import signature

# this logic is only here for the purpose of illustration
def lift(receiver, prop, cb):
    # noinspection PyUnusedLocal
    def callback(attr, old, new):
        setattr(receiver, prop, cb(new))
    return callback

bindings = {}
def bind(self, attr, cb):
    sig = signature(cb)
    params = sig.parameters
    for param in params.keys():
        bindings.setdefault(param, ).append(lift(self, attr, cb))
    setattr(self, attr, cb())

HasProps.bind = bind # monkey patch
# example begins proper

def data(power=0.1):
    """More pythonic callback (returns value instead of side-effect)"""
   xs = [x * 0.005 for x in range(200)]
    ys = [x ** power for x in xs]
    return dict(x=xs, y=ys)

source = ColumnDataSource()
source.bind('data', data) # binds to target instead of sources

plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

slider = Slider(start=0., end=4., value=.1, step=.1, title='power', name='power')

curdoc().add_root(column(slider, plot))

# Now bind callback to receivers, would move to a utility routine
for sender_name, callbacks in bindings.items():
    sender = curdoc().get_model_by_name(sender_name)
    sender.on_change('value', *callbacks)
</pre>

As you can see, source.bind replaces slider.on_change. The implementation identifies sender(s) by inspecting the callback signature.

Please share your thoughts on this idea. If there's enough interest, I will submit a PR. Folks more familiar with bokeh internals should feel free to suggest better implementation ideas.

Hi Jan,

The seems interesting, and similar notions have at least been raised in other contexts. I am not sure it's the style I'd use personally, so I don't have strong feelings about it. But I will say:

* If it can be added as an optional lightweight convenience layer on top of the existing machinery (which seems to be the case from your sample code), and

* Extensive tests to prevent any regressions in the future were included, and

* Docs, including a small discussion of the two approaches, and at least one example, were added

Then we'd be happy to consider a PR.

Thanks,

Bryan

···

On Apr 9, 2018, at 13:02, Jan Burgy <[email protected]> wrote:

The current callback API forces one to register callbacks on their senders. As a consequence, and also because callbacks rely on side-effects, intended receivers are not always obvious, particularly when the callback code is long. I propose an alternative, receiver-centric API. Here's a possible implementation for illustration:

<pre>
from bokeh.layouts import column
from bokeh.core.has_props import HasProps
from bokeh.models import ColumnDataSource, Slider
from bokeh.plotting import curdoc, figure
from inspect import signature

# this logic is only here for the purpose of illustration
def lift(receiver, prop, cb):
   # noinspection PyUnusedLocal
   def callback(attr, old, new):
       setattr(receiver, prop, cb(new))
   return callback

bindings = {}
def bind(self, attr, cb):
   sig = signature(cb)
   params = sig.parameters
   for param in params.keys():
       bindings.setdefault(param, ).append(lift(self, attr, cb))
   setattr(self, attr, cb())

HasProps.bind = bind # monkey patch
# example begins proper

def data(power=0.1):
   """More pythonic callback (returns value instead of side-effect)"""
  xs = [x * 0.005 for x in range(200)]
   ys = [x ** power for x in xs]
   return dict(x=xs, y=ys)

source = ColumnDataSource()
source.bind('data', data) # binds to target instead of sources

plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

slider = Slider(start=0., end=4., value=.1, step=.1, title='power', name='power')

curdoc().add_root(column(slider, plot))

# Now bind callback to receivers, would move to a utility routine
for sender_name, callbacks in bindings.items():
   sender = curdoc().get_model_by_name(sender_name)
   sender.on_change('value', *callbacks)
</pre>

As you can see, source.bind replaces slider.on_change. The implementation identifies sender(s) by inspecting the callback signature.

Please share your thoughts on this idea. If there's enough interest, I will submit a PR. Folks more familiar with bokeh internals should feel free to suggest better implementation ideas.

--
You received this message because you are subscribed to the Google Groups "Bokeh Discussion - Public" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit https://groups.google.com/a/continuum.io/d/msgid/bokeh/4091305b-9375-49f7-b006-854987f49216%40continuum.io.
For more options, visit https://groups.google.com/a/continuum.io/d/optout.