Live Cells C++
Reactive Programming for C++
|
This library provides a number of tools for building expressions of cells without requiring a computed cell to be created explicitly using live_cells::computed()
.
The arithmetic and relational (<
, <=
, >
, >=
) operators, when applied to cells holding numeric values, return cells which compute the result of the expression.
This allows a computation to be defined directly as an expression of cells. For example the following cell computes the sum of two cells:
live_cells::computed()
but is also more efficient since the argument cells are determined at compile-time.The sum
cell is a cell like any other. It can be observed by a watch function or can appear as an argument in a computed cell.
Expressions of cells can be arbitrarily complex:
a + 1
, are converted to constant cells, as if by live_cells::value()
.Every cell overloads the ==
and !=
operators, which return cells that compare whether the values of the cells are equal or not equal, respectively.
The following operator overloads and functions are provided for cells holding boolean-like value:
&&
||
!
select
&&
and ||
operators overloads preserve the short-circuiting behaviour of the operators. This means the value of a cell is not referenced if the result of the expression is already known without it.The third argument (if false) of live_cells::select()
can be omitted, in which case the cell's value will not be updated if the condition is false:
The computation of a computed cell's value can be aborted using live_cells::none()
. When live_cells::none()
is called inside a computed cell, the value computation function is exited and the cell's current value is preserved. This can be used to prevent a cell's value from being recomputed when a condition is not met:
live_cells::none()
is called during the first call to the cell's computation function, the default value for the cell's value type is retained.live_cells::none()
only preserves the current value of the cell, but this might not be the latest value of the cell if the cell's value is only referenced conditionally. A good rule of thumb is to use live_cells::none()
only to prevent a cell from holding an invalid value.If an exception is thrown during the computation of a cell's value, it is rethrown when the value is referenced. This allows exceptions to be handled using try
and catch
inside computed cells:
The live_cells::on_error()
function creates a cell that selects the value of another cell when an exception is thrown.
A template overload of on_error
is provided, which takes an exception type as a template type parameter. When this overload is used only exceptions of the given type are handled.
The validation logic in the previous example can be implemented more succinctly using:
live_cells::value(false)
to create a constant cell that holds the values false
.The previous value of a cell can be accessed using live_cells::previous
:
This results in the following being printed to standard output:
live_cells::previous
returns a cell that can be used like any other cell. When previous
is called multiple times on the same argument cell, the same cell is returned.
prev
does not hold a value. Accessing it will throw a live_cells::uninitialized_cell_error
exception.prev
must have at least one observer in order for it to keep track of the previous value of a
.What if you want to use the value of a cell in a computed cell but don't want changes to that cell's value triggering a recomputation? The live_cells::peek()
function allows you to do exactly that.
In the above example cell c
is a computed cell referencing the value of a
and peeks the value of b
. Changing the value of a
triggers a recomputation of c
, and hence triggers the watch function which prints to standard output, but changing the value of b
doesn't trigger a recomputation of c
.
live_cells::peek()
returns a cell.You may be asking why do we need live_cells::peek()
here instead of just accessing the value of b
directly using b.value()
. Something we've glossed over till this point is the lifecycle of cells. Cells are only active while they are actually observed, and are activated when the first observer is added. While active, cells react to changes in their argument cells. When the last observer is removed, cells are deactivated and stop observing their argument cells. When a new observer is added, they are reactivated again. Essentially, this means that the value of a cell may no longer be current if it doesn't have at least one observer. For a computed cell this is not a problem, since when it is inactive it computes its value on demand, but it may cause issues with other cells. The live_cells::peek()
function takes care of adding an observer to the peeked cell, so that it remains active, but at the same time prevents the observers, added through live_cells::peek()
, from being notified of changes in its value.
The live_cells::ops
namespace provides a collection of operators which can be applied on cells using the |
operator.
For example the select
function, introduced earlier, can also be applied on the condition cell with the following:
which is equivalent to:
The live_cells::ops
package also provides a variant of on_error
, peek
and previous
that can be used with the pipe operator. This allows for a reactive pipeline to be built without nested function calls.
This is equivalent to:
The next section introduces two-way data flow.