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 Values 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))

For more detailed examples on how to use common flow controls, take a look at the client tests.
Copy link
On this page
cond
after
while_loop
Nested conditionals
Examples: updating a Tensor conditionally