Cell Expressions
This library provides a number of tools for building expressions of
cells without requiring a computed cell to be created explicitly with
live_cells.computed.
Arithmetic
The following arithmetic and relational operators/functions can be
applied directly on cells: <, <=, >, >=, +, -,
unary -, unary +, *, @, /, //, %,
divmod, **, <<, >>, &, |, ^, ~,
abs, round, math.trunc, math.floor, math.ceil.
Each operator returns a cell that applies the operator on the values
of the operand cells. This allows a computed cell to be defined
directly as an expression of cells. For example the following defines
a cell that computes the sum of two cells directly using the +
operator:
import live_cells as lc
a = lc.mutable(1)
b = lc.mutable(2)
c = a + b
print(c.value) # Prints 3
Note
This definition of the cell c is not only simpler than the
equivalent definition using live_cells.computed but is also
more efficient since the argument cells are known ahead of time.
c is a cell like any other cell. It can be observed by a watch
function or it can appear as an argument in a computed cell.
lc.watch(lambda: print(c()))
a.value = 5 # Prints 7
b.value = 4 # Prints 9
Expressions of cells can be arbitrarily complex:
x = a * b + c / d
y = x < e
Hint
To include a constant value in a cell expression, convert it to a
cell using live_cells.value
Logic and Selection
The following methods are provided by all cell objects:
a.logand(b)
Creates a cell that evaluates to the logical and of
aandb.
a.logor(b)
Create a cell that evaluates to the logical or of
aandb.
a.lognot()
Create a cell that evaluates to the logical not of
a.
a.select(b, c)
Create a cell that evaluates to the value of
bifaisTrueorcifaisFalse.
Note
logand and logor are short-circuting, which means the value
of the second operand cell is not referenced if the result of the
expression is already known without it.
import live_cells as lc
a = lc.mutable(False)
b = lc.mutable(False)
c = lc.mutable(1)
d = lc.mutable(2)
cond = a.logor(b)
cell = cond.select(c, d)
lc.watch(lambda: print(f'{cell()}'))
a.value = True # Prints 1
a.value = False # Prints 2
The second argument to select can be omitted, in which case the
cell’s value will not be updated if the condition is False.
import live_cells as lc
cond = lc.mutable(False)
a = lc.mutable(1)
cell = cond.select(a)
lc.watch(lambda: print(f'{cell()}'))
cond.value = True # Prints 1
a.value = 2 # Prints 2
cond.value = False # Prints 2
a.value = 4 # Prints 2
Aborting a computation
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.
Note
The select method from the previous section uses
live_cells.none to retain its current value when the
condition is False.
import live_cells as lc
a = lc.mutable(4)
b = lc.computed(lambda: a() if a() < 10 else lc.none())
lc.watch(lambda: print(f'{b()}')
a.value = 6 # Prints 6
a.value = 15 # Prints 6
a.value = 8 # Prints 8
If live_cells.none is called while computing the initial value
of the cell, the cell is initialized to the value provided in the
argument to live_cells.none, which defaults to None if no
argument is given.
Attention
The value of a computed cell is only computed if it is actually
referenced. 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 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.
Exception handling
When an exception is thrown while computing the value of a cell, it is
rethrown when the cell’s value is referenced. This allows exceptions
to be handled using try and except inside computed cells.
import live_cells as lc
text = lc.mutable('0')
n = lc.computed(lambda: int(text()))
@lc.computed
def is_valid():
try:
return n() > 0
except:
return False
print(is_valid.value) # Prints False
text.value = '5'
print(is_valid.value) # Prints True
text.value = 'not a number'
print(is_value.value) # Prints False
Cells provide two utility methods, on_error and error for
handling exceptions thrown in computed cells.
The on_error method creates a cell that selects the value of
another cell when an exception is thrown.
import live_cells as lc
text = lc.mutable('0')
m = lc.mutable(2)
n = lc.computed(lambda: int(text()))
result = n.on_error(m)
str.value = '3'
print(result.value) # Prints 3
str.value = 'not a number'
print(result.value) # Prints 2
on_error accepts an optional type argument. When a non-None
type is given only exceptions of the given type are handled.
result = n.on_error(m, type=ValueError)
The validation logic in the previous example can be implemented more succinctly using:
import live_cells as lc
text = lc.mutable('0')
n = lc.computed(lambda: int(text()))
is_valid = (n > lc.value(0)).on_error(lc.value(False))
The error method creates a cell that holds the last exception that
was raised or None if no exception has been raised:
error = n.error()
@lc.watch
def watch_errors():
if error() is not None:
print(f'Error: {error()}')
Like on_error this method also accepts a type argument. When
this argument is given, the cell evaluates to the exception raised
only if it is of the given exception type.
parse_error = n.error(type=ValueError)
error also accepts an all argument. When this is True, the
value of the error cell resets to None if the value of the cell
on which error is called changes its value such that it no longer
raises an exception. If all is False (the default), the value
of the error does not change if the cell on which error is
called does not raise an exception.
The difference between the two is demonstrated with the following example:
import live_cells as lc
text = mutable('0')
n = lc.computed(lambda: int(text()))
e1 = n.error() # all=False
e2 = n.error(all=True)
@lc.watch
def watch_errors():
print(f'\ntext = "{text()}")
print(f'error(all=False): {e1() is None}')
print(f'error(all=True): {e2() is None}')
text.value = 'not a number'
text.value = '10'
This results in the following being printed:
text = "0"
error(all=False): True
error(all=True): True
text = "not a number"
error(all=False): False
error(all=True): False
text = "10"
error(all=False): False
error(all=True): True
Peeking Cells
If you want to use the value of a cell in a computed cell but don’t
want changes in the cells value triggering a recomputation, access the
cell via the peek property.
import live_cells as lc
a = lc.mutable(0)
b = lc.mutable(1)
c = lc.computed(lambda: a() + b.peek())
lc.watch(lambda: print(f'{c()}'))
a.value = 3 # Prints: 4
b.value = 5 # Doesn't print anything
a.value = 7 # Prints: 13
In this example c is a computed cell that references the value of
a and peeks the value of b. Changing the value of a
causes the value of c to be recomputed, and hence the watch
function is called. However, changing the value of b does not
cause the value of c to be recomputed due to the value being
accessed via the peek property.
Note
peek is a property that returns a cell:
b = lc.mutable(1)
peek_b = b.peek
print(peek_b.value) # Prints 1
You may be asking why do we need peek instead of just accessing
the value of b directly using b.value. The reason for this is
due to the cell lifecycle. Cells are only active when they have at
least one observer.
When a cell is active it recomputes its value in response to changes in the values of its argument cells, if any. When a cell is inactive, it does not recompute it’s value when the values of its argument cells change. This means the value of a cell may no longer be current if it doesn’t have at least one observer.
The peek property returns a cell that takes care of observing the
peeked cell, so that it remains active, but at the same prevents the
observers, added through the cell returned by peek, from being
notified when its value changes.