Creating impure Values

In general, yarp Values are intended to be passed between simple, pure functions wrapped by the fn() decorator. Specifically, these functions don’t hold any state and the resulting Values change when-and-only-when any input Value changes. This type of function is very easy to write and reason about with yarp but is fundamentally constrained. For example, it is not possible to implement delay() using such a function since input :py;class:Value changes do not immediately result in the Value changing. Simillarly, the no_repeat() function also cannot be replicated since it doesn’t always change its output Value when its input changes.

To get around this limitation it is necessary to manipulate Values ‘by hand’. Lets begin by seeing how no_repeat() is implemented.

The following pseudo code implementation goes the ‘obvious’ implementation for a no-repeat value:

on source value changed:
    if source value != last source value:
        output value = source value
    last source value = source value

The actual Python implementation looks like:

>>> def no_repeat(source_value):
...     last_value = source_value.value
...
...     # Initially take on the source value
...     output_value = Value(last_value)
...
...     @source_value.on_value_changed
...     def on_source_value_changed(new_value):
...         nonlocal last_value
...         if new_value != last_value:
...             last_value = new_value
...             # Copy to output whether continuous or instantaneous
...             output_value._value = source_value.value
...             output_value.set_instantaneous_value(new_value)
...
...     return output_value

In this example we create function (or rather, a closure) called on_source_value_changed and set it as the callback for the source Value using Value.on_value_changed().

Note

This example uses the Python decorator syntax making the code read a little more naturally, as in the pseudo-code version.

The last_value variable is accessed from the enclosing scope is used to keep track of the last value received from the source. The nonlocal keyword is used to gain access to it from our callback.

The last detail is the way the output Value is updated. If source_value is a continuous function we could update the output using either:

output_value.value = new_value

Or:

output_value.value = source_value.value

However, if source_value is an instantaneous value, we’d need to do use Value.set_instantaneous_value():

output_value.set_instantaneous_value(new_value)

Since we’d like to make our output Value mimic the input regardless of whether it is continuous or instantaneous, instead we use the following two-step process:

output_value._value = source_value.value
output_value.set_instantaneous_value(new_value)

By setting _value we change Value.value without triggering any callbacks registered with Value.on_value_changed(). We set this to the continuous value of the source (which is :py:data`NoValue` if the source is instantaneous). By calling Value.set_instantaneous_value() with the just-received value from the source we cause the callback to occur in the output Value.

You can try it out, first lets try a continuous value:

>>> # Create a value to de-repeat
>>> v = Value(123)
>>> nrv = no_repeat(v)
>>> nrv.on_value_changed(print)
<built-in function print>

>>> # Repeated values should not pass through
>>> v.value = 321
321
>>> v.value = 321
>>> v.value = 321
>>> v.value = 123
123

Next lets try an instantaneous value:

>>> # Create another instantaneous value to de-repeat
>>> iv = Value()
>>> nriv = no_repeat(iv)
>>> nriv.on_value_changed(print)
<built-in function print>

>>> nriv.value is NoValue
True

>>> iv.set_instantaneous_value(123)
123
>>> nriv.value is NoValue
True

>>> iv.set_instantaneous_value(123)
>>> nriv.value is NoValue
True

>>> iv.set_instantaneous_value(321)
321
>>> nriv.value is NoValue
True