Comment on page
Flow control: after, cond, while_loop
When using Python to develop a TinyChain service, it’s important to remember that the output of your code is a compute graph which will be served by a TinyChain host; your Python code itself won’t be running in production. This means that you can’t use Python control flow operators like
if
or while
the way that you’re used to. For example:@tc.get_op
def to_feet(txn, meters: tc.Number) -> tc.Number:
# IMPORTANT! don't use Python's if statement! use tc.cond!
return tc.cond(
meters >= 0,
meters * 3.28,
tc.error.BadRequest("negative distance is not supported"))
It’s also important to keep in mind that TinyChain by default resolves all dependencies concurrently, and does not resolve unused dependencies. Consider this function:
@tc.post_op
def num_rows(txn):
max_len = 100
schema = tc.table.Schema(
[tc.Column("user_id", tc.Number)],
[tc.Column("name", tc.String, max_len), tc.Column("email", tc.String, max_len)])
txn.table = tc.table.Table(schema)
txn.table.insert((123,), ("Bob", "[email protected]"))
return txn.table.count()
This Op will always resolve to zero. This may seem counterintuitive at first, because you can obviously see the
table.insert
statement, but notice that the return value table.count
does not actually depend on table.insert
; table.insert
is only intended to create a side-effect, so its result is unused. To handle situations like this, use the after
flow control:@tc.post_op
def num_rows(txn):
max_len = 100
schema = tc.schema.Table(
[tc.Column("user_id", tc.Number)],
[tc.Column("name", tc.String, max_len), tc.Column("email", tc.String, max_len)])
txn.table = tc.Table(schema)
return tc.after(
txn.table.insert((123,), ("Bob", "[email protected]")),
txn.table.count())
Now, since the program explicitly indicates that
table.count
depends on a side-effect of table.insert
, TinyChain won’t execute table.count
until after the call to table.insert
has completed successfully.Loops are probably the most difficult part of TinyChain to get used to if you've never used a graph runtime before.
Consider this simple while loop:
i = 0
while i < 10:
i += 1
In a TinyChain compute graph, you have to account for the facts that a) you need the loop to run at execution time (when a user executes your graph), not encoding time (when the TinyChain Python client exports your graph configuration as JSON), and b) TinyChain
Value
s are immutable:import tinychain as tc
@tc.get_op
def loop(until: tc.Number) -> tc.Int:
# the closure decorator captures referenced states from the outer scope,
# in this case "until"
@tc.closure
@tc.post_op
def cond(i: tc.Int):
return i < until
@tc.post_op
def step(i: tc.Int) -> tc.Int:
return tc.Map(i=i + 1) # here we return the new state of the loop
initial_state = tc.Map(i=0) # here we set the initial state of the loop
# return the loop itself
return tc.while_loop(cond, step, initial_state)
Consider this example:
@tc.post_op
def maybe_delete(table: tc.table.Table, should_update: tc.Bool):
a = tc.cond(should_update,
table.update({"column": "value"}),
table.delete())
# this won't work!
return tc.cond(table.is_empty(), tc.BadRequest("empty table"), a)
In this case, TinyChain is unable to resolve the dependencies of the state to
return
without executing both branches of a
, which would make a
no longer a conditional (the table
would be deleted!). For this reason, nested conditionals are not allowed. The most foolproof way to handle a nested conditional is to use an Op
:@tc.post_op
def maybe_delete(table: tc.table.Table, should_update: tc.Bool):
return tc.cond(should_update,
table.update({"column": "value"}),
table.delete())
@tc.post_op
def check_result(table: tc.table.Table, should_update: tc.Bool):
return tc.cond(table.is_empty(),
tc.error.BadRequest("empty table"),
maybe_delete(table, should_update))
Last modified 1yr ago