Chapter 12

Introducing Custom Classes

This chapter puts the concepts of class and object into a blender to produce some apparently confusing facts: a class is an object (while Object is a class), and Class itself is a class. It's not as confusing as it sounds. You'll learn how the Telescript environment uses class objects (instances of Class) to define other objects. You'll see how a Telescript engine stores and uses class objects and the class definitions that go with them, and you'll learn the fundamentals of declaring your own classes.

This chapter, the first of this section, is strictly an overview to help you understand the fundamentals of custom classes and the syntax of class declaration. You'll find full details about class declaration in the chapters that follow.

Class Mechanisms in the Telescript Environment

A class concept that you read about in earlier chapters bears repeating here: every object on a Telescript engine is defined by a class. The object itself is simply a stored state--the values of the object's properties. Whenever an operation is called on an object (including getting or setting an attribute), the engine finds the object's class and executes the appropriate method maintained with it for the operation. If the method executed reads or writes to a property, it changes the object's (not the class's) property and thus affects the object's state.

It should come as no surprise to you, given Telescript's deeply object-oriented nature, that a class is itself an object--it is an instance of the class Class. Each class that exists on a Telescript engine is a Class object that defines any instances of that class.

Class Definitions

Every class object on a Telescript engine is associated with a class definition. The class definition specifies the structure and workings of any instances of the class. The definition has two parts:

When you declare a custom class, you specify a class definition using the Telescript syntax described in this and later chapters. When the declaration is compiled and executed, the engine creates a class object for the custom class and maintains the class definition in association with that class object. The class definition is not an attribute of the class object; the engine uses a hidden mechanism to maintain the class definition. This means that you don't have direct access to the class definition of a class object--only the engine can work directly with class definitions.

Class Names

The class Class has a single attribute, name, that stores a class name. A class name is an object that distinguishes one class object (one instance of Class) from all other class objects.

The class name is an instance of the class ClassName. A class name has a single attribute, classDigest, that contains an octet string that denotes a class. That octet string is created by the engine when a class object is instantiated, and may be any octet string that the engine's current class-naming mechanism supports. Engines at this writing simply create an octet string that is the ASCII equivalent of the text of the class's name. For example, if you declare a custom class named "Traveller", the engine creates a class name for it whose classDigest attribute is the octet string "54726176656c6c6572", the ASCII equivalent of "Traveller". It's important to note, though, that the class-naming mechanism may change with different implementations of the Telescript engine, and isn't guaranteed to reflect the text of the class name.

Class Maintenance and Public Packages

A Telescript engine maintains all classes using public packages. Packages are a process mechanism that you can read more about in Chapter <<<X>>> of the last section of this book. For now, it's enough to know that a public package is a dictionary of objects that is maintained as an attribute of a place. Any places nested within that place--or any occupants of the place or any of its nested subplaces--have access to the public packages so they can retrieve references to objects in the packages. You might find it easiest to think of a public package as a sort of place bulletin board that you can be assured is available to all subplaces and occupants.

When you create a custom class, the engine instantiates Class to create a class object for your class. It places the class object in a package of classes that you can include in the public packages of a place. Because the package of classes is a dictionary, it stores key-value pairs. Each class object in a class package is stored as a value. It's associated with a key that is the class name of the object so that the engine can easily retrieve the class by supplying its class name as the key. The public package containing classes, like all public packages is protected. It can be read but it can't be changed.

Class Instantiation

When you instantiate a class in a constructor expression, the engine uses the class name of the class to find the appropriate class object in the public packages of a place. It calls the operation new on the class object, which accepts any initialization arguments and returns a newly constructed instance of that class--an instance constructed according to the class definition associated with the class object.

Every object has a class attribute that is the class object used to construct the object. When you use any of the object's features, the engine finds the object's class in the public packages and uses the class definition associated with the class to execute the method associated with the feature. Thus it's important that an object's class is in the public packages available in the place that the object occupies.

Class Scope

Every class in a Telescript engine has a scope that depends on where the public package storing that class is located. Every Telescript built-in class--that is, all the classes directly defined by Telescript language--are guaranteed to be located in the public packages of every engine place. Because the engine place is the outermost place on every engine (it contains all other places on the engine and isn't contained by any other place), the classes in its public packages are available to every subplace and occupant of the engine place. In other words, built-in classes are available to every process running on an engine. And because every engine is guaranteed to have built-in classes in the public packages of its engine place, this means that built-in classes have a global scope--they're available everywhere in the telesphere.

The scope of custom classes depends on the place where they're stored. Most custom classes have local scope--that is, they're only be available to the processes within one place (and subplaces thereof) of a single engine. That one place is typically the engine place, so the classes are available to every process in that engine--but not to processes on another engine.

As an example, consider Figure 12-1. The class CreditCheck is one of many classes located in a class package that's one of Engine Place's public packages. That class has a scope that is Engine Place, Market Place, Gizmos Place, Gadgets Place, Information Place, Daily News Place, Weekly News Place, and all agents in those places--in other words, all places and all agents on the engine. The class ShipOrder is one of several classes in Market Place. Its scope is Market Place, Gizmos Place, Gadgets Place, and all agents in those places. Its scope doesn't include Engine Place, Information Place and its subplaces, and any agents in those places. GatherNews is a class of likewise limited scope--limited to Information Place, its subplaces, and all agents in those places.

Figure 12-1 : The scope of a class includes the place where it's posted, all subplaces of that place, and all agents in the place and its subplaces. (Rectangles are places, circles are agents.)

The idea of class scope brings up an interesting question in an agent-based language: what happens to an agent defined using local classes when that agent travels to a new location where those classes may not exist? The answer is: its custom classes travel with it. When go is called on the agent, the agent ceases execution and the engine encodes the agent's state before transmitting the agent to a new location. As part of the encoding process, the engine encodes the custom classes (and associated class definitions) used to instantiate any of the objects that are part of the agent.

When the encoded agent arrives at its new location, the engine there decodes the agent's custom classes and includes them in the public place of the destination location. When the agent resumes execution in its new location, the custom classes it depends on will be available to it.

Class Duration

When custom classes are defined and assigned to the public packages of a place, those classes remain in that place for the lifetime of the place.

Declaring a Simple Class (A Practical Example)

Now that you're familiar with Telescript's class mechanisms, it's time to learn the basics of the syntax necessary to create your own custom classes. The piece of sample code that follows shows you how to declare an extremely simple class. The lines of code are numbered so that you can examine the parts of the class declaration individually.

1	do{
2		*@Place.publicPackages.include(Sampleclasses: module = (
3			Speaker: class = (
4				public
5					initialize: op() = {
6						^;
7					};

8					speakUp: op() = {
9						"Attention, please!".dump();
10					};
11			);
12		));

13	<<[:Speaker]speakUp>>;
14	}
When you execute this script, it defines and creates the custom class Speaker, puts it in the public packages of a place, then instantiates the class and calls the operation speakUp on the speaker object. You see these results:

String: <Attention, please!>
Now let's take a look at the parts of the script and how they work.

Using a Module Declaration

The full script of the example is a simple do block. The contents of the block start in line 2 with an operation call on a place. The call extends from line 2 through line 12. When executed, the operation includes the newly defined class in the public packages of a place. We'll ignore the call for the moment and concentrate instead on the single argument for that operation. That argument starts with the word SampleClasses in line 2 and ends with the first parenthesis in line 12. The argument is a module declaration.

A module declaration is a Telescript expression that includes class declarations and class interfaces (a truncated form of class declaration described later) within parentheses. When the declaration is executed, it returns a class package that can be included in the public packages of a place. The package returned is the kind of class package you just read about: it has as its values a class object for each newly defined class, and each of those values has a matching key that is the class name of the class object.

The syntax for a module declaration is this:

<ID>: module = (<class declaration(s)>)
The identifier names the package that's returned when the module declaration is executed. The module keyword that follows specifies that this is a module expression. The classes declared in the module all appear as one or more class declarations within the parentheses of the expression. (You may also put class interfaces within the module, something you'll read about in Chapter 13, which discusses modules in detail.)

Modules in the Real World

It's important to realize that there is no such thing as a module object. When a module declaration expression is evaluated, it returns a package of classes. You can think of that package as a module if you wish, but there is no class Module, and hence no module objects.

The package returned by a module declaration does not put its newly defined classes into effect until it's included in the public packages of a place. That's not a function carried out by a module declaration. You typically include a module package into a place's public packages using an external Telescript programming tool such as a loader. The loader can include module packages in the public packages of an already executing engine place so the newly defined custom classes can be used anywhere within the engine place.

For our example script, we use a little chicanery that you probably won't use in real Telescript programming. We call the include operation directly on the boot place. The boot place is a place created by an engine when you execute a solitary do block after compiling it. (At least that's what happens in the current implementation of Telescript--it's not guaranteed to happen in future implementations.) Because we know that the boot place is executing the contents of the do block, we can post the new class package in the public packages of the boot place. We do so in the second line of the example code:

*@Place.publicPackages.include(<module epxression> );
The responder of this operation is *@Place. In other words, the responder is the current object (*, the boot place) which is asserted to be a place (@Place). Every place has the attribute publicPackages, which is a list of public packages associated with the place. By calling the list operation include on that attribute, the statement includes the package created by the module declaration into the public packages of the boot place. Any classes declared in the module declaration are now available within the boot place.

Using a Class Declaration

The module declaration in the sample code contains a single class declaration in lines 3-11. It declares a class named Speaker that contains two features: an initialize operation and an operation--speakUp--that simply dumps a string that says "Attention, please!" This simple declaration shows the main components of a class declaration.. All class declarations follows this syntax:

<ID>: <class characteristics> class <superclasses> = (<features>);

Setting an Identifier

The identifier at the beginning of a class declaration becomes the new class's class ID. You use the class ID in your source code to specify the newly defined class wherever you want to use the class: in inheritance, when specifying type, in constructor expressions, and so forth. Within the source code, the class ID distinguishes the class from all other classes.

It's important to realize that the class ID of a class is not the same thing as the class name of a class. The class ID of a class is a string defined by Telescript syntax convention; the string identifies the class in source code. The class name is an object that distinguishes the class object of a newly created class from all other class objects. Although the current implementation of the Telescript engine uses the class ID to create a new class's class name (as described earlier), the mechanism used to create class names isn't guaranteed to do so, and may not in future implementations of the engine. The moral is this: don't expect the class ID to have anything to do with the string that is the class name.

When you choose a class ID for a class expression, you may not use the class IDs of built-in Telescript classes such as Object, Agent, and so on.

Specifying Class Characteristics

As you'll recall from the introduction to classes in Chapter 2, a class can be a flavor or a mix-in. It can be concrete so you can instantiate it, or abstract so you can't. It can be unsealed so you can create subclasses of the class, or sealed so you can't. These are the class's characteristics. You can set them using keywords immediately following the semicolon after the class ID.

The keywords that control class characteristics are:

You can use one or two of these keywords together to specify class characteristics with this restriction: abstract and mixin can't be used together--they're mutually exclusive. That's because a mix-in by definition can't be an abstract class.

Here's an example:

Porcino: sealed mixin class = (<features>);
The class Porcino is specified to be a sealed mix-in class. If you leave out all characterics keywords, you specify an unsealed concrete flavor class. That's the case in the example code, where speaker is declared without any of these keywords.

Specifying Inheritance

If you want your newly defined class to inherit from specific classes, you can specify those superclasses by class ID after the class keyword of the class expression. The superclasses are enclosed in parentheses. If you use more than one superclass, their class IDs are separated by commas.

You can specify superclasses for either flavor or mix-in classes; the conventions are a bit different for each kind of class.

Flavor Classes

If you specify one or more immediate superclasses for a flavor class, the first class must be a flavor class. The following classes (if there are any) must be mix-in classes. (As you'll learn in Chapter <<<X>>>, the order of mix-in classes here is important, especially if they contain different implementations of the same feature. The order determines which implementation prevails in the new class.)

Here's an example of a class declaration that declares a class that inherits from the flavor class Place and the mix-ins MeetingPlace and EventProcess (in that order):

SwapMeet: class (Place, MeetingPlace, EventProcess) = (<features>);

Mix-In Classes

If you declare a mix-in class and specify one or more superclasses, it's important to realize that a mix-in class can't inherit from a flavor class. It can only inherit from other mix-in classes, which can be specified as its superclasses. Here's an example of a mix-in class inheriting from two other mixin classes:

TheWorks: mixin class (PackageProcess, PermitProcess) = (<features>);
In this case, the new mix-in class TheWorks inherits features from the mix-ins PackageProcess and PermitProcess and then defines whatever other features it wishes in its declaration.

You can specify a flavor class in a mix-in's inheritance by placing it as the first class in parentheses. When you do so, you don't specify a superclass, you specify the mix-in class's anchor class. A mix-in's anchor class restricts when that mix-in may be used in the inheritance of other class declarations: the mix-in may only be mixed in with its anchor class or with a subclass of that anchor class. To see how it works, consider this example:

Notifiable: mixin class (Agent, EventProcess) = (<features>);
It specifies a mix-in class that inherits from the mix-in superclass EventProcess, and has Agent as its anchor class. It may only be used to define a subclass (or subclasses thereof) of its anchor Agent. In other words, it may only be used as a mix-in superclass of agent subclasses.

If a mix-in specifies only an anchor class, the mix-in doesn't inherit from any classes (it's completely self-defined), and it's anchored to the specified flavor class. Consider this example:

Notifiable: mixin class (Agent) = (<features>);
It declares a mix-in class that can only be used as a mix-in superclass of agent subclasses, as in the previous example. It doesn't inherit any features from other mix-in classes.

Not Specifying Inheritance

If you leave out superclasses (and their enclosing parentheses) from a flavor class declaration, you specify Object as the new class's immediate superclass with no mix-in superclasses. If you leave out superclasses from a mix-in class declaration, the new mix-in class has no superclasses at all.

In the code example earlier, the flavor class Speaker is declared without superclasses, so it inherits directly from Object.

Declaring a Class's Features

On the right side of a class declaration's equal sign, embraced by a pair of parentheses, are the class's feature declarations. Each feature is declared using syntax described later in this chapter.

Class Declaration Evaluation

A class declaration, like a module declaration, returns a package when executed. In the case of a class declaration, the package contains a single class object as its sole value, which is paired with a key that is the class object's class name. When a module declaration executes, all the class declarations are consolidated to produce a single package containing all the newly defined classes.

Class Families

A class declaration can declare a class family as well as a class. To do so, it adds a list of formals and uses those formals throughout the class declaration. You can read about the specifics of class family declaration syntax in Chapter <<<X>>>.

Declaring Features

Within the parentheses on the right side of a class declaration lie all the feature declarations of the class. To declare features within those parentheses, you must use feature-declaration syntax that includes four types of elements:

These four elements may be mixed in any order within the class expression's parentheses. The order of the operation declarations, attribute declarations, and sealing declarations has no effect on the final class declaration. For example, an operation declared at the end of the class declaration can call an operation declared at the beginning of the class declaration with no problem. The order of feature keywords among the operation and attribute declarations does matter. As you'll see, a keyword affects all following operation and attribute declarations until another keyword appears that overrides the first keyword.

Because Telescript syntax is riddled with semicolons, and proper semicolon placement is important to writing compilable Telescript code, here are the rules for semicolon placement using feature elements: Operation declarations, attribute declarations, and sealing declarations must all end with a semicolon. Feature keywords do not end with a semicolon.

Operation and attribute declarations each have their own detailed syntax that we'll discuss in more detail later in this chapter. Feature keywords and sealing declarations are much simpler, so we'll talk about them here.

Feature Keywords

A feature keyword sets a feature declaration parameter that remains in force within the class declaration until the end of the class declaration or until another keyword appears that overrides the first keyword. There are two groups of feature keywords: requester keywords and responder keywords.

Requester Keywords

Requester keywords specify who can request any of the features declared after the keyword.

(If you need more details about the public and private features of Telescript objects, you'll find them in Chapter 2.)

If you use the keyword public in the implementation of a class declaration, it remains in force until another requester keyword appears. All feature declarations that occur while public is in force declare public features. If you use the keyword private, it too remains in force until another requester keyword appears. All feature declarations that occur while private is in force declare private features. For example, consider the following code snippet:

Foo: class = (
	private

	<operation declaration>
	<operation declaration>
	<attribute declaration>

	public

	<attribute declaration>
	<operation declaration>
);
The keyword private in the second line means that the next three feature declarations declare private features. The keyword public that follows overrides the first keyword, and specifies that the next two feature declarations declare public features.

Any feature declarations that aren't preceded by either private or public are declared by default to be private features.

Responder Keywords

A class declaration's feature declarations can create features for several types of objects:

You can use responder keywords to control which of those object types a feature will affect. The keywords are these:

Note that any feature declarations that aren't preceded by class, instance, or property are declared by default to be instance features.

Instance and Class Keywords

Let's first examine the keywords instance and class. If you use the keyword instance in the implementation of a class declaration, it remains in force until another responder keyword appears. All attribute and operation declarations that follow it are features declared for instances of the defined class.

If you use the keyword class in the implementation of a class declaration, it too remains in force until another responder keyword appears. All attribute and operation declarations that follow it are features declared for the class object created by the class declaration.

To see how instance features and class features differ, consider the following example.

Widget: class = (
	class

	<operation foo declaration>
	<attribute bar declaration>

	instance

	<operation fubar declaration>
	<operation rabuf declaration>
);
The keyword class at the beginning of the implementation is in force for the declarations of the operation foo and the attribute bar, so these two features are class features of the class object Widget. This is the class object that is stored in the public packages of a place where the class is in effect.

The keyword instance is in force for the declarations of the operations fubar and rabuf, so both operations are instance features.

Once the class declaration is compiled and executed, the module package created containing the Widget class object is included in the public packages of the engine place. Let's say that you create an instance of Widget and name it gizmo. You can call the operations fubar and rabuf on gizmo because they are instance features and are available to any instances of the class Widget. These operations are also available to any instances of subclasses of Widget because the operations are inherited. If you try to call either the operation foo or the operation bar on gizmo, you'll get a compiler error because those operations aren't instance features, and aren't available to instances of Widget.

If you call the operation class on gizmo, you'll receive the class object Widget as a return value. You can call either foo or bar on the class object, because they are both class features. You can't call either fubar or rabuf on the class object, because they are instance features that aren't available to the class object created according to the class declaration.

The Property Keyword

Now let's turn to the keyword property. If you use it in the implementation of a class declaration, it remains in force until another responder keyword appears. While in force, any attribute declarations that follow it are turned into property declarations. There are no class properties, so all property declarations declare instance properties.

Any operation definitions that follow the property keyword ignore the keyword. The last class or instance keyword that was in force applies to operation declarations while property remains in force for attribute declarations. If, for example, if instance is in force and is followed by the property keyword, any operation declarations following property declare instance operations. Any attribute declarations declare properties--until class or instance appear again.

As you'll learn later in this chapter, an attribute declaration has one or more IDs, a constraint (a class ID), and other optional attribute elements that can include a custom getter and setter. When the class is created, the engine creates a property to be used by the the attribute's getter and (optionally) setter. When the property property keyword precedes an attribute declaration, the engine ignores everything in the declaration except for the ID (or IDs) and the constraint. The engine creates a property that is associated with the ID, an object that satisfies the type specified by the constraint of the declaration. You'll read more about how a property is declared later in this chapter and in Chapter <<<X>>>.

Feature Keywords in Combination

Although requester and responder keywords can be in force simultaneously, not all combinations of requester and responder specification have meaning. In particular, public and private, which define features, have no effect on property declarations because properties are not features.

Sealing Declarations

You can, as you'll see, declare any attribute or operation of a class to be a sealed or an unsealed feature. Telescript syntax also allows features to be sealed outside of their declarations in a single element called a sealing declaration.

A sealing declaration begins by listing the IDs of any features to be sealed. Its syntax is the following:

<feature ID(s)>: sealed;
The feature IDs at the beginning of the declaration are the IDs used at the beginning of each feature's declaration. You can use one or more IDs here. If you use more than one, you separate the IDs from each other using commas. Here's an example of a sealing declaration that seals three features:

hop, skip, jump: sealed;
The sealing declaration is a useful convention for sealing many of a class's features at once, but beware of one pitfall: Many programmers read through feature declarations to understand how they can work with individual features of the class. If you seal a feature in a sealing declaration, it isn't at all apparent in the feature declaration that the feature is sealed. Another programmer reading the feature declaration may believe the feature is unsealed.

Declaring an Operation

An operation is the fundamental means of communication between two objects. An operation call can pass objects from the requester to the responder, and can return an object from the responder to the requester when the operation is finished. Any work the object can do is defined by its operations, any of which can throw an exception during execution.

To accommodate the many aspects of an operation, the syntax of an operation declaration has many elements, some of them optional. We won't discuss them in full detail here, but we will take a look at the overall structure of an operation declaration. The syntax of an operation declaration is:

<ID(s)> : [<characteristics>] op ([args>]) [[<result constraint>] [throws <exception class ID(s)>] [= <block>];
(The optional syntactic elements are enclosed here in square braces ([]) that aren't characters you use in an operation declaration.)

The first thing to notice is that an operation declaration, like a class expression, is divided into its interface (its signature--its ID, the arguments it passes, the result it returns, any possible exceptions thrown, and so on) and its implementation (its method--a block of statements and variable declarations that execute when the operation is called). The two parts are separated by an equal sign; the signature is on the left of the equal sign, the method is on the right of the equal sign.

You can see an example of the simplest form of an operation declaration in a code snippet from the sample code at the beginning of the chapter:

speakUp: op() = {
	"Attention, please!".dump();
};
In this example, the signature (the interface) is simply speakUp: op(). It uses no keywords and has no specified arguments nor a result nor any specified exceptions. Its method (the implementation) is a one statement block that dumps a string.

Note that the implementation of an operation (the equal sign and the block that follows it) may be omitted. If so, you declare the interface alone of an operation. You can read more about operation interface declaration in Chapter <<<X>>>.

Operation IDs

You start an operation declaration with one or more identifiers. Most operation declarations use a single ID. In that case, the ID becomes the name of the operation. The name is used when calling the operation.

Some operation declarations use more than one ID, in which case they declare multiple operations, one for each ID. Each of the operations has a distinct ID, but a declaration that is otherwise identical to the other operations. For example, consider this code snippet:

speakUp, pipeUp, squawk: op() = {
	"Attention, please!".dump();
};
It declares three operations: speakUp, pipeUp, and Squawk. Each operation does exactly the same thing when called: it dumps the string "Attention, please!"

Characteristics

You have the option of specifying operation characteristics by using any one of three keywords immediately following the colon of the operation declaration. They are:

The idea behind an abstract operation is that you may want to define the interface of an operation that must be a part of each subclass of this class, but you don't want to provide an implementation. Each subclass must provide its own implementation. By declaring an abstract operation, you create such an operation. If you try to provide an implementation for an abstract operation by including an equal sign and a method at the end of the declaration, the engine will ignore the implementation. <<<The compiler may balk at this.>>> If you don't use the abstract keyword, the operation is concrete--it has an implementation that must be defined in this class declaration.

Sealed operations, as you read earlier, are exactly the opposite of abstract operations. They require an implementation in this class declaration, and the implementation can't be overridden in subclasses. Remember that you can seal an operation using a keyword here or by using a sealing declaration as described earlier. If you don't use the sealed keyword, the operation is unsealed unless it's sealed in a sealing declaration.

abstract and sealed are mutually exclusive keywords; you can't use them together in an operation declaration because an operation can't be both abstract and sealed.

sponsored can be used in combination with either abstract or sealed. Sponsored and unsponsored operations deal with authority, a topic that's covered in the section of this book that discusses processes. You can read about the specifics of sponsorship there. For now, it's enough to know that if you use the sponsored keyword, you specify a sponsored operation. If you don't use the keyword, you specify an unsponsored operation.

Arguments

A pair of parentheses follows the op element of an operation declaration. You may specify expected arguments within the parentheses--zero, one, or more arguments. If you want the operation to expect no arguments, you leave the parentheses empty as in the operation signature from the sample code before:

speakUp: op() = <method>;
If you want the operation to expect arguments, you specify the arguments within the parentheses. You read an introduction to argument specifications in Chapter 4, where you learned to read operation signatures so you could supply the appropriate arguments when you called an operation on an object. We won't go into the full details of argument specification here--it's too involved for this overview chapter--but we will look at some examples and come up with some general rules.

Every argument specified within the parentheses of the operation declaration must have at least a constraint. A constraint, as you'll remember from Chapter 4, specifies a type (a class) that the argument object must be a member of, and a passage of the argument--whether it's passed by copy, by reference, and so on. Each argument may or may not have one or more IDs associated with it. If the argument has no ID, it's an unnamed argument. If it has one or more IDs, its a named argument (or arguments if it has two or more IDs). Look at an example:

readIt: op(firstPart, secondPart: copied Dictionary; thirdPart: List;
	Boolean) = <method>;
This operation expects four arguments. The first two are the named arguments firstPart and secondPart, both created by IDs associated with the constraint copied Dictionary. This means that both arguments must be satisfied by objects that are members of the class Dictionary, and that are passed as copies of the original objects. The third argument, thirdPart, is a named argument satisfied by a member of the class List that doesn't have its passage specified--therefore it must be passed by reference (the default passage). The fourth argument has a constraint but no associated ID, so it is an unnamed argument that must be satisified by a member of the class Boolean passed by reference.

You can specify a named or unnamed variable-number argument by using an ellipsis (...). The ellipsis follows the constraint; it specifies an argument that may be satisfied by zero, one, or more objects that satisfy the constraint. For example,

readIt: op(readings: Number ...) = <method>;
specifies a named variable-number argument readings that can be satisfied with zero, one, or more objects that are members of the class Number. A variable-number argument can be either named or unnamed.

You'll see the reason for named and unnamed arguments in Chapters 15 and <<<X>>>, where you see how argument values are used in methods and for operation escalation. You'll find the full set of argument syntax rules in Chapter 15.

A Result

An operation may be declared to return a result or not, as you desire. If you specify a result, you supply a result constraint immediately after the right parenthesis of the arguments. The result constraint is the same as an argument constraint--it consists of a type and a passage. The single object that is returned as the operation's result must be a member of class specified by type, and must be passed according to the passage specification. (Like argument constraints, if there is no passage keyword in the constraint, passage is by reference.)

The method of the operation can supply the result using a return statement (described in Chapter <<<X>>>.) If the method doesn't use a return statement, the value returned is the value of the method itself (treating it as a block expression).

If you don't want to specify a result for the operation, you simply leave out a result constraint in the operation declaration. Note that if you don't specify a result for an operation and then you use a return statement in the operation's method, the compiler will emit a great squawk and refuse to compile your code.

Exceptions

As you learned in Chapter 7, any block (and hence the method of any operation) can throw an exception using a throw statement. You can specify what exceptions an operation may throw by adding the keyword throws to an operation declaration, following it with the class IDs of one or more exception classes that may be thrown. The keyword and the class IDs come before the equal sign of the class declaration. If there is more than one class ID, they are separated from each other by commas. If you don't want to specify any exceptions, leave out the keyword and accompanying class IDs.

Specifying possible exceptions thrown by an operation isn't a rigorously defined part of the operation declaration. The exception class IDs you list are advisory only--the operation may throw any number of exceptions that aren't listed. For example, if you try to divide by zero within the operation's method, the operation will throw a DivisionByZero exception, an exception that isn't created by a throw statement within the method. The operation will throw that exception whether or not it's specified, and will throw it even if no exceptions are specified.

It's a good idea to specify any exceptions that may be thrown by throw statements within the operation's method. This shows programmers using the operation what special exceptions they may encounter when using the operation.

The Method (the Implementation Block)

Once you've declared an operation's interface on the left side of the equal sign, you can supply the operation's implemention--its method--on the right side of the equal sign. The method is simply a block. It follows the block syntax you learned about in the last section, with a few special statements you'll learn about in Chapter 16 that allow the method to accept operation argument values and return a result.

Simply explained, arguments and results work like this: If, within a method, you declare a variable that has the same ID as a named argument in the operation's signature, that variable is assigned the argument value when the operation is called. That is, it does as long as you don't assign the variable a value when you first declare it. This syntax allows the method to receive the argument values necessary for its execution.

As discussed earlier, a method returns a result to the operation's requester by using a return statement or (without a return statement) by simply returning the value of the method itself. The returned object must satisfy the constraint specified in the operation signature.

Here's an example, an operation declaration that defines an operation that accepts arguments, returns a result, and may throw an exception:

1	sum: sealed op( figures: Number ... ) Number throws SumTooHigh = {
2		total: Number = 0;
3		for figureValue: Number in figures {
4			total = total + figureValue;
5		};
6		if total > 1000 {throw SumTooHigh();};
7		return total;
8	};
The operation's interface is defined in line 1. Its ID is sum. It's a sealed operation that can't be overridden in subclasses. It specifies a variable-number argument, figures, that can be any number of integers and real numbers (all members of class Number). It also specifies a returned value that must satisfy the constraint Number. It advises that its method may throw the exception SumTooHigh.

The operation's implementation (its method) is defined in lines 2 through 7. Lines 2 through 5 make use of a variable, total, and a for/in block to sum up the numbers supplied for the variable-number argument figures. Notice that the variable figures isn't declared anywhere within the method. That's because every named operation argument is automatically declared as a variable for the method and is assigned whatever value (or values) were supplied for the argument. This is the mechanism that passes an operation's arguments into its method. It's discussed in more detail in Chapter <<<X>>>.

Line 6 tests to see if the sum of the figures is more than 1000, and throws an instance of the custom exception SumTooHigh if so. This is the exception that was specified in the operation's interface.

Line 7 executes if the sum wasn't too high. It returns the sum of the arguments, a value that will be either an integer or a real number, which satisfies the returned value's constraint specified in the interface.

Declaring an Attribute

As you'll remember from Chapter 2, an attribute is typically a property with one or two attendant operations: the getter and the setter. The getter gets the value of the property; the setter sets the value of the property. A read-only attribute has only a getter, and can't be set.

Declaring an attribute is simpler than declaring an operation. You don't have to worry about arguments because you can never call an attribute--you can only set it in an assignment statement, or read it in a feature expression. The syntax of an attribute declaration is this:

<ID(s)> : [<characteristics>] constraint> [throws <exception class ID(s)>] [with (<getter/setter declarations>)];
Like an operation declaration, an attribute declaration has many optional syntactic elements, enclosed here in square braces. An attribute declaration in its simplest form consists of a single ID followed by a colon, a constraint, and a semicolon. For example,

keepTab: Integer;
declares the read/write attribute keepTab as an integer that is passed by reference (the default of a constraint if no passage is specified). The attribute uses the default getter and setter, which simply read from or write to the property associated with the attribute.

The optional elements of an attribute declaration allow you to customize a standard attribute. The keywords let you create abstract, sealed, sponsored, or read-only attributes; the throws clause lets you specify exceptions that might be thrown; and the with clause lets you define a custom getter and setter.

Operation IDs

You can use one or more IDs in an attribute declaration, just as you can in an operation declaration. The IDs are separated by commas if there is more than one. Each ID declares its own attribute.

Characteristics

You can specify the characteristics of an attribute using these keywords:

These keywords, with the exception of readonly, are the same keywords you use with an operation declaration and have the same affect on an attribute's getter and setter (if there is one) that they have on an operation's method. abstract and sealed are mutually exclusive keywords; you can't use them together because an attribute can't be both abstract and sealed. The other keywords may be used together in combination. For example:

ridges: sealed readonly Integer;
This statement declares a read-only attribute whose getter can't be overridden in subclasses.

The Constraint

An attribute declaration's constraint specifies the type and passage of the value received from the attribute's getter and of the value given to the attribute's setter (if it has one). The constraint is a mandatory part of the attribute declaration.

Exceptions

An attribute can specify one or more exceptions that may be thrown by its getter and setter just as an operation may specify exceptions that may be thrown by its method. You simply add the keyword throws after the constraint, followed by the class IDs of one or more exception classes that might be thrown. If there's more than one exception class ID, you separate them with commas.

This exception clause is advisory, just as it is in an operation declaration. It's typically not used unless you define a custom getter or setter.

Custom Getters and Setters

You can, if you wish, supply a custom getter or setter (or both) for an attribute--as long as the attribute isn't abstract. To do so, you add the optional with clause at the end of the attribute declaration. The clause starts with the keyword with followed by a pair of parentheses that encloses one or two operation declarations. These operation declarations follow the syntax you just read about earlier in the chapter. An operation here must have the ID get (to create a custom getter) or set (to create a custom setter). If you have two operations, one must be get, the other set.

Custom get and set operations must satisfy the constraint set by the attribute declaration. You can read full details about custom getters and setters in Chapter <<<X>>>.

Declaring a Property

As you read earlier, if you use the feature keyword property in a class declaration, any attribute declarations that follow while the keyword is in force become property declarations. If that's the case, then only the ID (or IDs) and the constraint of the attribute declaration is considered. Each ID declares its own property.

A property takes the type specified by the constraint, but it ignores the passage. That's because all properties are passed by reference.

Properties can be used by the methods of a class, but can't be used by the methods of subclasses--a convenient aspect of scope that you can read more about in Chapter 16 when you read about methods.

Class Classes

This redundantly-titled section closes the chapter by describing the two classes at the heart of Telescript's class mechanism: Class and ClassName. The class tree that follows shows their inheritance:

Object

* Class (Protected)
* ClassName

The class Class is defined using the mixin Protected which, as you'll recall, immediately locks any instance of a subclass so that it can't be changed.

The Class Class

Class, as you read earlier in the chapter, defines a class object that defines other objects--the instances of the class. To be used for object instantiation and definition, class objects must be contained in a public package of a place where the instantiated objects will reside (or in the public package of a place containing that place).

Constructing a Class

You can't construct a class object as you do most other objects--that is, through a constructor expression. If you try using a constructor expression directly using the class Class, it throws the exception FeatureUnavailable. That's because class objects may only be instantiated using the class declarations described earlier in this chapter--syntax that ensures that the class object of a newly defined class is included in a package that can be included in the public packages of a place.

You can, if you wish, copy an already constructed class object. If you do so, the copy has the same inheritance as the original class object.

Naming a Class

When you declare a class, you give it a class ID to differentiate the class from other classes in your source code. The Telescript engine, when creating a class object for the class, creates a class name object and assigns it to the new class object. Class defines a single attribute to hold the class name object:

A class object's class name is used when the class object is included in a package: the class name is the key, the class object is its associated value.

Checking Inheritance

Class offers two operations that let you check the inheritance relationship between two classes:

These two operations allow you to check to see if one class object is a subclass of a second class. This expression, for example,

25.class.isSubclass(Number)
returns true because25 is an integer, and Integer is a subclass of Number.

Instantiating a Class

Class offers one operation that lets you instantiate the class:

This operation throws the exception ClassAbstract if the class is an abstract class and can't be instantiated. If the class's initialize operation fails, it can throw the exceptions ObjectUninitialized or Exception.

As an example of instantiation, consider the following operation expression where new is called on the class object String:

String.new("Instant Instance")
The expression returns a string object that has the contents "Instant Instance". "Instant Instance" was the argument used to initialize the new string object.

If this example looks familiar to you, consider this constructor expression:

String("Instant Instance")
It accomplishes the same instantiation task as the previous expression. As it turns out, the syntax of a constructor expression is a convenient syntactic shorthand for calling the new operation on a class object. The two expressions are identical in execution and result.

The ClassName Class

ClassName defines an object that distinguishes an associated class (its subject class) from all other classes, whether they're built-in or custom classes. This is not a class that most programmers instantiate--it's typically used by the engine to create a class name object for a new class object created by a class declaration. ClassName has no operations, and a single attribute:

To construct a class name, you use a constructor expression in which you supply an owned octet string as its sole argument. The octet string is used to set the classDigest attribute. The Telescript engine uses a particular algorithm for choosing the appropriate octet string for a subject class. Because the algorithm isn't defined as part of High Telescript syntax, programmers shouldn't try to anticipate it. It's simplest to leave class name assignment to the engine. It's a rare case, in fact, that programmers will have any need to instantiate class names for themselves.

* * *

With this introduction, you should have a good sense of the overall syntax necessary for declaring a custom class. You should, in fact, be able to declare simple classes using the information presented here. The chapters that follow go into the full detail you'll need as you learn to declare more involved custom classes.

12 - Introducing Custom Classes
Class Mechanisms in the Telescript Environment
Class Definitions
Class Names
Class Maintenance and Public Packages
Class Instantiation
Class Scope
Class Duration
Declaring a Simple Class (A Practical Example)
Using a Module Declaration
Modules in the Real World
Using a Class Declaration
Setting an Identifier
Specifying Class Characteristics
Specifying Inheritance
Flavor Classes
Mix-In Classes
Not Specifying Inheritance
Declaring a Class's Features
Class Declaration Evaluation
Class Families
Declaring Features
Feature Keywords
Requester Keywords
Responder Keywords
Instance and Class Keywords
The Property Keyword
Feature Keywords in Combination
Sealing Declarations
Declaring an Operation
Operation IDs
Characteristics
Arguments
A Result
Exceptions
The Method (the Implementation Block)
Declaring an Attribute
Operation IDs
Characteristics
The Constraint
Exceptions
Custom Getters and Setters
Declaring a Property
Class Classes
The Class Class
Constructing a Class
Naming a Class
Checking Inheritance
Instantiating a Class
The ClassName Class

TS Ref - 26 JUN 1996

Generated with Harlequin WebMaker