Using Values and Events

The two fundamental types in yarp are Value and Event. While basic uses of these are shown in the introduction, and the API docs describe how they work, the purpose of this page is to show some of the different ways that these should be used in practice.

Using fn and yarp utilities

The most straightforward way to build things with yarp is to transform Values and Events using pure functions and fn, and use the utilities in yarp to add stateful behaviours where necessary.

Say for example we have a function motion_sensor(), which returns an Event which emits whenever motion is detected in a room, and a function set_lights(Value) which accepts a Value containing a bool, and turns the lights on or off depending on that value, whenever it changes.

We can use time_window and fn to make a simple timer-controlled light behaviour:

# an Event representing detected motion
motion_event = motion_sensor()

# a Value containing a list of the values emitted by motion_event in the
# last minute
recent_motion = time_window(motion_event, 60)

# a function which takes the list of recent motion events (as a `Value`)
# and returns True if it contains any events
@fn
def any_motion(recent_motion):
    return len(recent_motion) > 0

# a Value containing a bool, indicating whether the lights should be on or off
light_state = any_motion(recent_motion)
# continuously set the lights to the desired state
set_lights(light_state)

In this case it would be possible to implement without fn, using yarp’s wrapped built-ins and operator overloading, for example:

# yarp.len returns a Value, which has an overloaded > operator, which also
# returns a Value
light_state = yarp.len(recent_motion) > 0

A case where this might actually be necessary is if we wanted to add a way to override the light state. For example if we have a function get_override() which returns a Value containing None (no override) or True/False (override to on or off):

@fn
def apply_override(input, override):
    if override is None:
        return input
    else:
        return override

set_lights(apply_override(light_state, get_override()))

Here, apply_override takes two Values, and combines them into one with the override logic applied.

There’s more that can be done with fn (see the documentation, it can handle Events too), and this technique can get you quite far, but what if you need to implement your own stateful things, or the input/output functions used above?

Using Values

The exact behaviours of Value are described in the API documentation, but in practice there are 5 main ways to interact with them (beyond using fn, overloads and utilities in yarp), shown in the examples below.

Source Values

These represent the value of some external state, and have no dependencies:

def get_value():
    v = Value(initial_value=get_current_state())

    def some_callback(new_state):
        # later, inside an asyncio callback indicating that the state has changed
        v.value = new_state

    something.register_callback(some_callback)

    return v

Value.value is set in some asyncio callback; this runs its on_value_changed callbacks, informing listeners that the value has changed.

Output Values

Functions like set_lights in the example above take a Value and make changes to other systems or the real world depending based in its value. For example if change_light_state takes a plain bool and turns the light on/off:

def set_lights(state):
    change_light_state(state.value)

    state.on_value_changed(change_light_state)

Note that it’s possible that after calling this, there are no references to state, so it may be garbage collected (bad). Real implementations must either keep a reference to the input values in functions like set_lights, or make sure to keep references to the values passed to them:

# bad
set_lights(apply_override(...))

# good
light_state = apply_override(...)
set_lights(light_state)

Functional Values

These represent a transformation of some other value. These should generally be made with fn or overloads instead:

>>> x = Value(1)
>>> y = Value(inputs=(x,), get_value=lambda: x.value + 1)
>>> y.value
2
>>> x.value = 3
>>> y.value
4

Any input values used in get_value must be specified in inputs.

Stateful Values Using get_value

This is when state is mutated state using callbacks, but updated in get_value. This is particularly useful when merging multiple values to avoid updating the value more than once in a transaction.

>>> def history(input_value):
...     '''a value containing the historic values of input_value'''
...     values = [input_value.value]
...     input_value.on_value_changed(values.append)
...     return Value(inputs=(input_value,), get_value=values.copy)

>>> x = Value(1)
>>> h = history(x)
>>> h.value
[1]
>>> x.value = 2
>>> h.value
[1, 2]

All Values and Events which mutate the state must be listed in inputs, otherwise get_value will never be called and the result will not update.

Stateful Values by Setting .value

This is when Value.value is written directly in a callback. This is most useful when the value reacts to both input values (specified in inputs) and external events (e.g. asyncio callbacks).

>>> def integrate(input_value):
...     '''get a value containing the sum of all input values of
...      input_value over time'''
...     result = Value(input_value.value, inputs=(input_value,))
...
...     @input_value.on_value_changed
...     def on_value_changed(new_value):
...         result.value = result.value + new_value
...
...     return result

>>> x = Value(0)
>>> i = integrate(x)
>>> i.value
0
>>> x.value = 2
>>> x.value = 3
>>> i.value
5

Using Events

Events largely follow the same patterns as Values, with minor differences. Again, see the API documentation first for the exact semantics.

Source Events

These represent events from something outside yarp, and have no dependencies:

def get_event():
    e = Event()

    def some_callback(value):
        # later, inside an asyncio callback indicating that something has happened
        e.emit(value)

    something.register_callback(some_callback)

    return e

Event.emit is called in some asyncio callback; this runs its Event.on_event callbacks, informing listeners that something has happened.

Output Events

It’s possible (though perhaps unusual) to define functions that take an Event and make something happen in the real world when it emits a value:

def do_something_on_event(event):
    event.on_event(actually_do_something)

The same warnings about dangling references in Output Values apply here too.

Functional Events

These represent a transformation of some other event. These should generally be made with fn or overloads instead:

>>> x = Event()
>>> y = Event(inputs=(x,))
>>> x.on_event(lambda value: y.emit(value + 1))
<...>
>>> y.on_event(print)  # normally you'd do something else with y here
<...>
>>> x.emit(1)
2

Any Event or Value that can trigger y.emit should be listed in the inputs of y.

Stateful Events

This is largely the same as above, but using some nonlocal state:

>>> def diff(input):
...     last = 0
...
...     result = Event(inputs=(input,))
...
...     @input.on_event
...     def on_event(value):
...         nonlocal last
...         result.emit(value - last)
...         last = value
...
...     return result

>>> x = Event()
>>> y = diff(x)
>>> y.on_event(print)  # normally you'd do something else with y here
<...>
>>> x.emit(1)
1
>>> x.emit(5)
4

Using on_inputs_done

When combining Events and Values it may be necessary to use on_inputs_done to get the correct behaviour. This callback runs when all Values or Events listed in inputs have finished updating. If it is not used when combining Events and Values, then value updates that happen in the same transaction as the event may be missed. For example, this shows one way this can go wrong:

>>> def add_badly(value, event):
...     result = Event(inputs=(value, event))
...
...     @event.on_event
...     def on_event(event_value):
...         result.emit(value.value + event_value)
...
...     return result

>>> e = Event()
>>> ee = Event(inputs=(e,))
>>> e.on_event(ee.emit)
<...>
>>> v = event_to_value(e, initial_value=0)
>>> s = add_badly(v, ee)
>>> s.on_event(print)
<...>
>>> e.emit(2)
2
>>> e.emit(2)
4

What’s going on here? When e emits, the first callback is for ee.emit, which in turn triggers on_emit in add_badly, which finally calls result.emit. This all happens before the second callback registered on e (in event_to_value) which updates v.value, so on_event sees the old value of v (0), and emits the wrong value.

In the second call to e.emit, the old value was again used, but this time it’s accidentally correct.

This seems contrived (it is, particularly as the problem doesn’t appear if ee is made using fn because of lucky dependency ordering), but can happen in real-world situations.

How to solve this problem?

First, just use fn, it handles this for you.

To implement things like fn, though, use on_inputs_done to emit events after inputs have finished updating:

>>> def add_goodly(value, event):
...     event_buf = []
...     event.on_event(event_buf.append)
...
...     def on_inputs_done(emit):
...         for ev in event_buf:
...             emit(value.value + ev)
...         event_buf.clear()
...
...     return Event(inputs=(value, event), on_inputs_done=on_inputs_done)

>>> e = Event()
>>> ee = Event(inputs=(e,))
>>> e.on_event(ee.emit)
<...>
>>> v = event_to_value(e, initial_value=0)
>>> s = add_goodly(v, ee)
>>> s.on_event(print)
<...>
>>> e.emit(2)
4

The chain of callbacks described above (e -> ee -> event.on_event in add_goodly) still happens, but now events aren’t emitted from s until e, ee and v have finished updating, so the result is as expected.

Note that events are buffered in a list. This is because events can emit more than once in a transaction. on_inputs_done is called with the Event.emit function of the Event that it’s registered to, to avoid a circular reference.

Transactions

Transactions are mentioned a few times above, but what are they and how do they work?

Transactions exist only to run the get_value and on_inputs_done callbacks on Values and Events, once the objects listed in their inputs have finished updating.

These callbacks are necessary to have a place to update a Value or Event once, even if there is a diamond in the graph of dependencies, as in the example above.

Transactions are started automatically, and consist of the following steps:

  • Something outside of yarp initiates a change to a Value or Event (called the “initial object” below), by setting Value.value or calling Event.emit. This starts the transaction.

  • Change recording starts: any Value or Event objects which are changed are recorded.

  • The on_value_changed or on_event callbacks of the initial object are ran, possibly causing updates to dependent objects, which are recorded.

  • The get_value and on_inputs_done callbacks of transitive dependencies of the initial object are ran, in topologically-sorted order.

    Objects are skipped if none of their inputs have been marked as changed. Each of these callbacks may cause more callbacks to run, and thus more objects to be marked as changed.

  • Change recording stops, and the transaction ends.

The dependency information (inputs) is required to correctly order the dependencies. Some missing dependencies can be detected, but not all. When a dependency is missing, all callbacks will still run (possibly in a nested transaction, with a warning), but possibly more than once, or not in the order that solves the issue shown above.

It is sometimes not possible to specify all inputs in the Event or Value constructor. In that case, use Reactive.add_input.

Thread Safety

There is none – yarp is intended to be used without concurrency or with asynchronous programming. If you really want to use it with threads, either ensure that all yarp operations (constructing or updating objects) happen on only one thread, or are protected by a lock.