Lazy Signals¶
In this library, there are signals and effects. Signals are containers that hold a single value. Effects are functions that are called when any signal they depend on changes its value.
Warning
Read this if you came to the impression that the library just implements the observer pattern.
Define Signals¶
There are two major ways to define a new signal:
Define a variable-style signal by using the
Signalconstructor. You can pass in the initial value and update it by assigning to thevalueproperty.Build a signal from others by using the
func()decorator. You write a function that uses thevalueproperty of other signals to compute a live value.
Top Level Signals¶
- class Signal(value=None)
A container to hold a reactive value.
- property value
- Getter:
gets the current value, registering the current effect as a dependent
- Setter:
sets the current value, notifying all dependent effects if it changed
You can create a signal by calling the constructor.
Optionally, you can pass in the initial value.
An instance of this class very much behaves like a normal variable:
Simply use signal.value to access (get/set) its value.
You must wrap your variables in this class to make them reactive.
- Example:
from lazysignals import Signal s = Signal(0) # create a signal with initial value 0 print(s.value) # prints '0' s.value = 1 # update the value print(s.value) # prints '1'
Derived Signals¶
- derived(fn)
Define a new signal whose value is computed by
fn().- Parameters:
fn – A function to compute the value from.
- Returns:
A new
Signal.
Tip
Use as a function decorator.
Internally, this just defines an effect to update the derived signal via the provided function.
- Example:
from lazysignals import Signal, derived s = Signal(0) # create a signal with initial value 0 # compute `parity.value` via `s.value % 2` # `parity` now depends on `s` @derived def parity(): return s.value % 2 print(parity.value) # prints '0' s.value = 1 # update the value of `s` (not `parity`) print(parity.value) # prints '1'
Note
You do not need to define dependencies manually. However, they are only detected when they lie in the active code path. See the section on dependency detection for more details.
Tip
Only those signals are recomputed that may actually change. We call this lazy evaluation.
Define Effects¶
- effect(fn)
Run
fn()whenever (relevant) state changes.- Parameters:
fn – The function to run on state changes.
- Returns:
The return value of
fn().
Tip
Use as a function decorator.
The effect() decorator allows you to define a function that is called whenever one of the signals on which the decorated function depends changes.
- Example:
from lazysignals import Signal, derived s = Signal(0) # create a signal with initial value 0 t = 0 # create a normal variable with initial value 0 # define an effect that prints the value of `s` and `t` # immediately prints "0, 0" @effect def printer(): print(f"{s.value}, {t}") s.value = 1 # prints "1, 0" t = 1 # does not print anything s.value = 1 # does not print anything s.value = 2 # prints "2, 1"
The function fn is called immediately when the effect is defined.
Moreover, it is called whenever any of the signals it depends on changes its value.
Because of lazy evaluation, it is not called on assignments to other signals or those who do not actually change a value.
For obvious reasons, the effect is not called when variables change that are not defined as signals.
Note
You do not need to define dependencies manually. However, they are only detected when they lie in the active code path. See the section on dependency detection for more details.
Tip
You can use the effect() as a function decorator.
But you can also use as a function call, for example, effect(lambda: print(s.value)).
The call returns the result of evaluating the function.
A Note on Dependency Detection¶
With effects and derived signals, you do not need to define dependencies manually.
This is the magic with signals after all.
However, they are only detected when they lie in the active code path.
This means that if you mention a signal s in the function fn but do not make use of s.value, the dependency will not be detected.
For example if we have two signals s and t where t.value evaluates to False and we let fn = lambda: None if t.value else s.value, then t but not s will be tracked initially.
If, t.value now changes to True, causing an update call to fn for the derived signal, the new dependency on s will be recognized and respected down the line.
A Note on Exceptions¶
Whenever exceptions are raised in a derived signal or effect, they are collected and raised at the end of the current signal propagation phase.
This exception takes the form of Exception(...list of raised exceptions...).