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
a
andb
.
a.logor(b)
Create a cell that evaluates to the logical or of
a
andb
.
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
b
ifa
isTrue
orc
ifa
isFalse
.
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.