Reactive Programming

Where event-driven programming is about reacting to things that happen, RP is about staying up to date with changing signals.

Classic event driven programming


In [ ]:
# The action to take upon a certain event is usually specified at the "source"
b = Button()
b.mouse_down.connect(some_callback)
...
def some_callback(event):
    ...

Reactive Programming (in flexx)

A signal can be created by decorating a function:


In [ ]:
from flexx import react

@react.connect('name')
def greet(n):
    print('hello %s!' % n)

Signals yield new values, thereby transforming or combining the upstream signals. Also, you can connect to as many signals as necessary:


In [ ]:
@react.connect('first_name', 'last_name')
def name(first, last):
    return '%s %s' % (first, last)

Input signals can be called with an argument to set their value:


In [ ]:
@react.input
def first_name(n='John'):
    assert isinstance(n, str)
    return n.capitalize()

@react.input
def last_name(n='Doe'):
    assert isinstance(n, str)
    return n.capitalize()

In [ ]:
# For the sake of the story, we defined the signals out of order, so we need to connect them
name.connect(); greet.connect()

In [ ]:
first_name()  # get signal value

In [ ]:
first_name('jane')  # set signal value (for input signals)

Observations:

  • The upstream signal (i.e. source) is specified at the callback function
  • The callback function is transformed into a signal
  • The signal yields new signal values, so you can create a stream/pipeline
  • Creating a pipeline provides a nice mechanism for caching values that take long to compute
  • Multiple upstream signals can be specified
  • It provides a nice integral way for user-provided data, as an alternative to properties or traits.

The HasSignals class


In [ ]:
class Item(react.HasSignals):
    
    @react.input
    def name(n):
        return str(n)

class Collection(react.HasSignals):

    @react.input
    def items(items):
        assert all([isinstance(i, Item) for i in items])
        return tuple(list(items))
    
    @react.input
    def ref(i):
        assert isinstance(i, Item)
        return i

In [ ]:
itemA, itemB, itemC, itemD = Item(name='A'), Item(name='B'), Item(name='C'), Item(name='D')
C1 = Collection(items=(itemA, itemB))
C2 = Collection(items=(itemC, itemD))

In [ ]:
itemB.name()

In [ ]:
C1.items()

Dynamism - connect to signals of signals


In [ ]:
class Collection2(Collection):
    
    @react.connect('ref.name')
    def show_ref_name(name):
        print('The ref is %s' % name)
    
    @react.connect('items.*.name')
    def show_index(*names):
        print('index: '+ ', '.join(names))

In [ ]:
itemA, itemB, itemC, itemD = Item(name='A'), Item(name='B'), Item(name='C'), Item(name='D')
C1 = Collection2(items=(itemA, itemB))
C2 = Collection2(items=(itemC, ))

In [ ]:
C1.ref(itemA)

In [ ]:
C1.ref(itemD)

In [ ]:
itemD.name('D-renamed')

In [ ]:
C2.items([itemC, itemD])

In [ ]:
itemC.name('C-renamed')

Lazy evaluation

By default react uses a push approach, which is useful in GUI's. In other situation, a pull approach might be more appropriate.


In [ ]:
@react.input
def foo(v):
    return str(v)

@react.lazy('foo')
def bar(v):
    print('update bar')
    return v * 10  # imagine that this is an expensive operation

In [ ]:
foo('hello')  # Does not trigger bar
foo('heya')
foo('hi')
bar()  # this is where bar gets updated

In [ ]:
bar()  # foo has not changed; cached value is returned

Signal history

Signals store their current value as well as the previous value. (The timestamps are also stored, though these are not yet available via the public API.)


In [ ]:
@react.input
def some_value(v=0):
    return float(v)
some_value(0)  # init

@react.connect('some_value')
def show_diff(s):
    print('diff: ', s - some_value.last_value)  # note: we might rename this to previous_value

In [ ]:
some_value(10)

In [ ]:
some_value(12)