Types and casting

TinyChain's Python client uses type declarations in an unusual way. Here's what to expect.

import os
import tinychain as tc

# use the demo host at demo.tinychain.net, unless overridden by the environment variable `TC_HOST`
HOST = tc.host.Host(os.getenv("TC_HOST", "http://demo.tinychain.net"))
# this endpoint will attempt to resolve whatever state you send it, without committing any changes
ENDPOINT = "/transact/hypothetical"

# this assumes that `x` is of type `tc.tensor.Tensor`
def average(x):
    return x.sum() / x.size

if __name__ == "__main__":
    cxt = tc.Context()  # initialize a new Op context
    cxt.x = tc.tensor.Dense.ones([3])  # initialize a new Dense tensor
    cxt.result = average(cxt.x)  # call our custom function

    actual = HOST.post(ENDPOINT, cxt)  # execute the `Op` defined by `cxt`
    assert actual == 1  # verify the result

The average function in this example works well enough, but in a public library it might not handle every situation it should. For example, if a user calls average(tc.URI("$x")), the sum method and size property won't be available because a URI doesn't have a sum method or size property. To handle cases like this, we can use TinyChain's built-in reflection annotations:

# ...
 
# the `post_op` annotation tells TinyChain to reflect over this function
# and assume that it defines a POST Op 
@tc.post_op
# the annotation on `x` tells TinyChain to expect a `Tensor`
def average(x: tc.tensor.Tensor) -> tc.Number:
    # the return annotation tells the calling code to expect a `Number`
    return x.sum() / x.size

if __name__ == "__main__":
    cxt = tc.Context()  # initialize a new Op context
    # `average` is now a TinyChain `Op`, not a native Python function,
    # so it needs to be made addressable by the calling context
    cxt.average = average
    cxt.x = tc.tensor.Dense.ones([3])  # initialize a new Dense tensor
    cxt.result = cxt.average(x=tc.URI("$x"))  # call our custom `Op`
    actual = HOST.post(ENDPOINT, cxt)  # execute the `Op` defined by `cxt`
    assert actual == 1  # verify the result

Now, even though the type of x in the calling code is a URI, the average Op still works as the caller expects because the type annotations tell the code in def average what to expect.

Important! In the TinyChain Python client, types define what to expect. They don't necessarily instantiate or cast types, like they do in native Python code.

To illustrate this, let's change the return type of average to a String:

# ...

@tc.post_op
def average(x: tc.tensor.Tensor) -> tc.String:  # <-- return type `String`
    return x.sum() / x.size

if __name__ == "__main__":
    # ...
    actual = HOST.post(ENDPOINT, cxt)
    assert actual == "1"  # this assertion fails!

Now, the return annotation tc.String tells the calling code to expect a String but the Op still actually returns a Number, so the assert fails. To fix this, we'll have to explicitly cast the return value:

# ...

@tc.post_op
def average(x: tc.tensor.Tensor) -> tc.String:
    avg = x.sum() / x.size
    return avg.cast(tc.String)  # explicitly cast `avg` to a `String`

if __name__ == "__main__":
    # ...
    actual = HOST.post(ENDPOINT, cxt)
    assert actual == "1"  # assert that the result is in fact a string

Last updated