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:

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:

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", "bob.roberts@example.com"))
    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:

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", "bob.roberts@example.com")),

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

def loop(until: tc.Number) -> tc.Int:
    # the closure decorator captures referenced states from the outer scope,
    # in this case "until"
    def cond(i: tc.Int):
        return i < until

    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)

Nested conditionals

Consider this example:

def maybe_delete(table: tc.table.Table, should_update: tc.Bool):
    a = tc.cond(should_update,
        table.update({"column": "value"}),

    # 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:

def maybe_delete(table: tc.table.Table, should_update: tc.Bool):
    return tc.cond(should_update,
        table.update({"column": "value"}),

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

Examples: updating a Tensor conditionally

For more detailed examples on how to use common flow controls, take a look at the client tests.

Last updated