The Cobj system (Field 14)

The Cobj (short for "context object") system is a Python & Java micro-framework for building small, tightly-coupled assemblages of objects that share a lot of information. In essence it's the magic associated with Field's _self variable re-factored out into pure Jython with an eye towards "dependency injection" like problems. We've come across situations that are well treated with this kind of approach in a whole variety of domains:

  • graphics — you have a bunch of lines and you want to set their color on all of them at once, and then override that on some of them. Additionally, you'd like a fairly uniform interface to all of these numbers so you can animate them using fairly general frameworks.
  • visualization — you have a collection of marker drawers that need to be connected to their data, their axes, their axes drawers and so on. Everything ends up coupled to around half of everything else.
  • UI — interface elements in every UI toolkit we've come across couple into the drawing hierarchy, mouse event hierarchy and keyboard focus hierarchy with varying degrees of automagicness. Sometimes these UI toolkits feel like they think they are solving this problem for the first time.

Field's _self framework comes from spotting this shared pattern, the Cobj system is a tiny (a single Java class file) reimplementation and refactoring of it that enables broader use of it.

tutorial_cobj.field

All the code for this page (together with its UnitTesting) can be found here: tutorial_cobj.field.zip.

A Cobj is a dict

The best way to understand this is just to see it in action. First, an import:

# this imports the base class
from field.context.Context import Cobj

Now we are free to make and assemble instances of Cobj:

a = Cobj(name="a")
print a.name 
#->a

A Cobj is a lot like an (open) object in Python, which in turn is a lot like a Dict. Anything can be the value of a property, and properties are created on demand

a.position = Vector3(1,2,3)

Access to missing properties is allowed — missing properties simply 'None'

print a.missing
#->None

This allows this pattern to access properties with default values:

something = a.missing or Vector3(1,2,3)

And this pattern to initialise things once:

a.wasMissing = a.wasMissing or Vector3(1,2,3)
print a.position
#->(1.0, 2.0, 3.0)

Finally, properties can be deleted using 'del':

del a.position
print a.position
#->None

Inheritance

So far, so boring. Now we get to the secret sauce

a = Cobj(name="a")
b = Cobj(name="b")
c = Cobj(name="c")

a.something = b
b.somethingElse = c

a.position = Vector3(1,2,3)

print a.something

But now "position" is visible in 'b' and 'c'

print c.position
#->(1.0, 2.0, 3.0)

print b.position
#->(1.0, 2.0, 3.0)

What is happening? Properties are resolved upwards (by depth first search):

b.position = Vector3(4,3,2)
print b.position
#->(4.0, 3.0, 2.0)

print c.position
#->(4.0, 3.0, 2.0)

del b.position
print c.position
#->(1.0, 2.0, 3.0)

This is the crux of the Cobj — a dynamically assembled tree of instances that delegate upwards to resolve properties.

Sometimes you need to see behind the curtain — where are you getting that property from? Prefixing a property name with "where_" tells you where it is being gotten from:

print a == c.where_position
#->True

This yields this pattern

c.where_position=Vector3(6,5,4)
print a.position
#->(6.0, 5.0, 4.0)

Which is sugar for

c.where_position.position=Vector3(6,5,4)

Subclassing

Subclassing in Python works exactly as expected — "self" gets all of the magic:

a = Cobj(masterValue=Vector3(1,2,3))

class B(Cobj):
    def doSomething(self):
        print self.masterValue

a.something = B()

a.something.doSomething()
#->(1.0, 2.0, 3.0)

But, if you'd rather not have every object in the tree see all of the properties of your objects, you can still hide some of them:

a = Cobj(masterValue=Vector3(1,2,3))

class B(Cobj):
    def __init__(self):
        # but you can still hide things from the tree if you like
        self.__dict__["hidden"]=3
    def doSomething(self):
        print self.masterValue
        print self.hidden

a.something = B()

a.something.somethingElse = Cobj()
print a.something.somethingElse.hidden
#->None

Computed properties "computed_"

First let's build a hierarchy:

a = Cobj(name="a")
a.elements = [Cobj(n=n) for n in range(0, 10)]
a.elements[3].furthermore = [Cobj(n=n*n) for n in range(10, 20)]

And set a "default" on the whole tree

a.position = Vector3(1,2,3)

As you might expect:

print a.elements[3].furthermore[2].position
#->(1.0, 2.0, 3.0)

But now we can add a "computed" property:

a.elements[3].furthermore[2].compute_position = lambda at,start : at.parent().position*2
print a.elements[3].furthermore[2].position
#->(2.0, 4.0, 6.0)

This function is called whenever 'position' looked up at this level of the tree — two parameters are passed in: the first is the location where we currently are in the tree (where this function is stored) the second is the place where this lookup started.

Arrays

Clearly, Cobj do something special when properties have Cobj values. But they also understand collections:

a = Cobj(name="a")
a.elements = [Cobj(n=n) for n in range(0, 10)]

This gets "name" from the common parent 'a'

print a.elements[3].name
#->a

down_

Usually property lookup is about traversing upwards through the hierarchy, but sometimes its useful to query this tree from top to bottom:

a = Cobj(name="a")
a.elements = [Cobj(n=n) for n in range(0, 10)]

print a.down_n
#->[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Properties prefixed with "down_" collect results over the tree (in depth first order). For example:

a.elements[3].furthermore = [Cobj(n=n*n) for n in range(10, 20)]

print a.down_n
#->[0, 1, 2, 3, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 4, 5, 6, 7, 8, 9]

There's also findAll, this runs a depth first predicate over the whole tree:

print [x.n for x in a.findAll(lambda x : x.n>4)]
#->[100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 5, 6, 7, 8, 9]

And, finally, there's .visit:

a = Cobj(name="a")
b = Cobj(name="b")

a.something = b

# this pattern can be useful to visit everything in a tree
def visitor(x):
    print x

a.depthFirst(visitor)

Dispatch

Cobj trees can masquerade as objects that implement Java interfaces. For this example we'll actually need a Java interface. We could get this from anywhere --- a Field built in class, a .jar from somebody else that we are using, or something we define this in Eclipse --- it just needs to be visible to Field.

Alternatively we could just make one in Field's embedded Java compiler:

For this to work you need to turn on the JavaC language support in the "lambda" tab in the inspector. With this defined we now have a Java interface.

print AnInterface
#-><type 'AnInterface'>

Python can use this. Here we make a subclass of Cobj that also implements AnInterface:

class MyCObj(Cobj, AnInterface):
    def doIt(self):
        print "I'm doing it! (%s)" % self.name

a = MyCObj(name="a")
b = MyCObj(name="b")

Obviously, we can just call methods on these things

a.doIt()
#->I'm doing it! (a)

a.something = b

Bt this syntax gets us an implementation of AnInterface that calls (depth first) everything in the hierarchy that implements AnInterface

everything = a.proxy(AnInterface)

everything.doIt()
# prints "I'm doing it! (a)" "I'm doing it! (b)"

Likewise:

a.somethingElse = [MyCObj(name=n) for n in range(0, 10)]

Having something that isn't AnInterface hanging out in the tree is not a problem:

a.notAnInterface = Cobj(name="neverCalled")

Note, we don't have to regenerate the proxy 'everything' when we change the tree.

everything.doIt()
# this runs a,b, and 0-9

These proxy objects can be passed around to any Java method that's expecting an AnInterface.

The underscore

Finally an optional, slightly more advanced feature — the underscore object.

from field.context.Context import _

This underscore allows you to refer to the currently constructed object while you are constructing it. Thus:

a = Cobj(root=_)

Is shorthand for:

a = Cobj()
a.root = a

This is especially useful when constructing lists using list comprehensions

[Cobj(datum=_, value=n) for n in range(0, 10)]

We end up doing this occasionally when we do dependency "injection" like things with this framework:

a = Cobj(root=_, masterValue=Vector3(1,2,3))

class B(Cobj):
    def doSomething(self):
        # here we lookup where to get values from using the name 'root'
        print self.root.masterValue
        # this is an extra level of flexibility from just saying self.masterValue

a.something = B()

a.something.doSomething()
#->(1.0, 2.0, 3.0)