An Introduction to the Tingle Programming Language

Dirk van Deun, dirk at dinf.vub.ac.be

Section 10: Companion Objects

The Tingle interpreter allows variable and method declarations at top level, outside of any nesting. These are stored in a singleton object named "*lobby*". The class of the object "*lobby*" has no name and is part of no name space. The "*lobby*" object responds to no messages. But the contents of "*lobby*" are always in scope: the outermost scope, just before the companion objects and the primitive functions.

Such top level entities are great for quick experiments at the prompt, but not meant to hold global entities in a program. For the latter purpose, companion objects are the clean alternative. Each class has a companion object associated with it: an object with the same name as the class, accessed through a scope even outside of that of the top level variables. As opposed to top level declarations, which like the primitive functions reside in all name spaces at once, companion objects do reside in the name space of their associated class.

Companion objects are explicit containers for methods and values that belong with a class, but not with a specific instance of it; more or less like class methods and class variables in some languages. We need only one extra keyword to declare them:

as bankAccount {
   var amountOfMoney
}

in bankAccount {
   var amountOfAccounts
}

"In" blocks can be nested in other "in" blocks in just the same way as "as" blocks can be nested in other "as" blocks; and as a convenience, they can be nested together in "at" blocks. ("At" blocks can only be nested in other "at" blocks, and can only contain further nestings, no declarations of their own.) Let us say we compose:

class BankAccount = bankAccount.

Then not only is the class BankAccount created out of the "as" part of bankAccount; but a second, nameless class (refered to as ~BankAccount in error messages) is created out of the "in" part, and then a singleton object of that latter class is created under the name BankAccount. So there will now be one instance of the variable "amountOfAccounts", part of the object BankAccount; and more generally only one per class that includes the idea bankAccount.

Companion objects have privileged access to the instances of their associated class; and instances have privileged access to the companion object of their class. Message sends between them are considered equivalent to direct self sends, making semi-protected methods of the companion object accessible to instances, and semi-protected methods of instances accessible to companion objects. The latter property makes companion objects fit as factories:

as counter {
   var value: get/semi set       // generate getter and setter
   def step() { value = value + 1 }
}

in counter {
   def make() { | object |       // object is a local variable
      object = new;              // create object of associated class
      object.set(0);             // use semi-protected setter
      object                     // return object
   }
}

class Counter = counter.

Note that "new" without a class name as an argument, meaning "make an object of the associated class", can avoid the need for slightly different make methods for several classes that an idea is part of. (When used in instance methods, it means "make another object of this class".)

This factory method was very wordy, so that lots of comments could be added. This is how you would normally write it:

in counter {
   def make() (new).set(0)
}

You make a new Counter using "Counter.make()". Companion objects can be refered to by their fully qualified name, of the form "Counter@main", or (usually) without the name space component. In the latter case, variables in scope with the same name can have precedence; but the convention of giving classes names that start with a capital letter makes this a non-issue.



Contents | Conclusion