Client-side Class Inheritance
Learn how to inherit from and modify the behavior of TinyChain client classes

Client-side methods

The recommended way to do object-oriented programming in TinyChain is to host classes as part of a service, as in the object orientation guide. This minimizes the amount of work the developer has to do in terms of keeping track of the difference between the compile-time and run-time state of their program. However, in some cases, instance methods must be defined entirely client-side, meaning they are defined and executed only at compile-time in order to construct Ops that are executed at run-time.
In general, you should use hosted class definitions as described in the object orientation and service hosting guides. The only situations where you should use this more advanced but much more challenging technique are when:
  1. 1.
    A trivial helper class is needed for convenience, such as assigning specific names and types to a Map or Tuple
  2. 2.
    Program efficiency depends strongly on compile-time parameters, or
  3. 3.
    The application cannot be distributed as a hosted service
For these reasons, client-side instance methods are used extensively in the TinyChain Python client. For example, Table.insert is only defined in the client (the host has no /insert handler in the host) because insert is not idempotent and therefore is not safe when replaying the low-level write operations recorded in a Block of a BlockChain. GraphTable then subclasses Table but, for efficiency, the GraphTable class must be defined according to the schema of the graph at compile-time. Code like this is not straightforward to read or write.

Easy: helper classes based on Map and Tuple

Consider this example:
1
@tc.post_op
2
def force(mass: tc.Tuple, acceleration: tc.Tuple) -> tc.Tuple:
3
mass_quantity = tc.Number(mass[0])
4
mass_unit = Mass(mass[1])
5
acceleration_quantity = tc.Number(acceleration[0])
6
acceleration_unit = Velocity(acceleration[1])**2
7
return mass_quantity * acceleration_quantity, mass_unit * acceleration_unit
Copied!
Because Tuple by itself doesn't contain any type information, there's a lot of boilerplate destructuring code here and a lot of room for error. For example, is the Unit of acceleration already Velocity**2? It's hard to see if there's a bug. Readability and usability can be improved by a helper class:
1
class UnitQuantity(tc.Tuple):
2
# note: there's no __uri__ defined here
3
# which means this class definition only exists in the Python client,
4
# i.e. it's not exported by any hosted service
5
6
@property
7
def quantity(self):
8
return tc.Number(self[0])
9
10
@property
11
def unit(self):
12
return Unit(self[1])
13
14
def __mul__(self):
15
# use self.__class__ as the return type here to better support subclasses
16
return self.__class__((
17
self.quantity * other.quantity,
18
self.unit * other.unit))
Copied!
Now the business logic is much more readable:
1
@tc.post_op
2
def force(mass: UnitQuantity, acceleration: UnitQuantity) -> UnitQuantity:
3
return mass * acceleration
Copied!

Challenging: construct a deep neural net

1
class NeuralNet(tc.Tuple):
2
"""abstract methods omitted here"""
3
4
class DNN(NeuralNet):
5
@classmethod
6
def load(cls, layers):
7
# the network architecture, like this parameter `n`,
8
# must be known at compile-time in order to construct
9
# an Optimizer for this ML model
10
n = len(layers)
11
12
# because the form of `DNN.forward` depends on `n`,
13
# the `DNN.forward` method must be defined at compile-time
14
class DNN(cls):
15
def forward(self, inputs):
16
17
# `layers` is defined in the compile-time context
18
# so even though we have access to `layers` here,
19
# we still have to reference `self[i]` instead of `layers`
20
21
# otherwise the ops could fail at run-time because
22
# they might depend on a state that was only defined
23
# at compile-time
24
25
state = self[0].forward(inputs)
26
for i in range(1, n):
27
state = self[i].forward(state)
28
29
return state
30
31
# here the form of the returned instance is set to `layers`
32
return DNN(layers)
Copied!
This example highlights the distinction between compile-time and run-time state which the developer must be mindful of in order to define a client-side instance method. As a general rule, a parameter which is a native Python state known at compile-time (like the integer n above) is safe to reference when constructing a class or method, but a TinyChain State in the calling context has to be referenced using self. The nested class idiom (where the create method defines a custom subclass of cls) is only necessary when the structure of a compute graph which the class defines depends on a compile-time parameter (like n in DNN.forward).