Anatomy of an Op
Understand when and how to use Contexts and type annotations
import tinychain as tc
# This is a native Python function.
# It doesn't need type annotations, although you can add them if you want,
# for example if they make the code more clear,
# or if you're using a linter which requires them.
# Since this is a native Python function, TinyChain code can only call it
# at compile-time.
a = 2
return b + a, b * a
# This is a TinyChain Op. It can be evaluated at run-time.
# Every TinyChain Op has its own Context, which you can access
# by using the reserved names "cxt" or "txn" as the first argument of the Op.
# Since this is meant to be called at run-time, TinyChain doesn't know
# at compile-time what type of arguments to expect. So an Op needs
# type annotations in order to provide the correct API when compiled to JSON.
# For example, if you leave out the tc.Number annotation, TinyChain will
# provide a generic tc.State when this Op is compiled, and you'll get an
# error that says that tc.State doesn't support the "+" or "*" operators.
def example2(cxt, b: tc.Number) -> tc.Tuple:
cxt.a = 2
return b + cxt.a, b * cxt.a
Notice the type annotation
Numberon the parameter
b. The actual value of parameter
bmay not be known when
exampleis encoded as part of a graph configuration, so the type annotation is necessary in order to provide the correct API methods from inside the Python function definition. For example, if someone calls
example(URI("http://example.com/numeric_constant")), this is perfectly valid code, but it doesn't explicitly provide the type of the
Likewise, the return type annotation
Tupleis provided for the benefit of the calling code. Without it, TinyChain would not know what type of
Stateto return at encoding time (when the compute graph configuration is generated) and helper methods like
unpackwould be unavailable.
cxtparameter in the
examplefunction above is a
Contextobject. It allows you to explicitly name the states which define your
Op—in this case, there is one state called
bpassed in as an argument and one state called
adefined in the body of the
Op. TensorFlow users should find this familiar because it provides the same functionality as the
name=keyword argument in TensorFlow.
Let's take another look at the
def example(cxt, b: tc.Number) -> tc.Int:
cxt.a = 2
return cxt.a + b, cxt.a * b
The Python interpreter is imperative and single-threaded, so as a Python developer you're probably accustomed to the assumption that each line of your code will execute in exactly the order that it's written. TinyChain, however, is multi-threaded and features automatic concurrency (like TensorFlow). So, in the example above,
cxt.a + band
cxt.a * bboth execute simultaneously when a user executes your compute graph.
Automatic concurrency is crucial for a performant distributed runtime, but it comes with some trade-offs. For example,
Values are immutable:
Tensorare mutable. This introduces the need to handle side-effects with the
Afterflow control, which we'll cover in the next section.