Chapter 4

Using an Object's Features

To work with aTelescript object, you use its features: you call its operations, you get and set its attributes. The basic syntax for using features is simple, but there are a few wrinkles you should know to get the results you want. This chapter explores those wrinkles. You'll learn how to specify an object and an attribute or an operation, how to supply arguments necessary for an operation, and how to handle an operation's result. You'll also--if you want to go into them--learn some advanced operation syntax: chaining and cascading operations, asserting object type when you call an operation, and escalating an operation.

Calling an Operation

Whenever an operation is called in the Telescript environment, there are two objects involved: the requester and the responder. The requester asks for the operation. It must specify the object and operation it wants, and it must supply any arguments that are necessary for the operation. If the operation returns a result, the requester receives the result and can deal with it in any way it sees fit.

The responder receives a request for an operation and, if the operation requires arguments, accepts the arguments supplied by the requester. The responder then executes the method associated with the called operation, using the accepted arguments as values assigned to variables within the method. If the method ends by producing a result, the responder returns the result to the requester.

This tango between requester and responder is at the heart of all Telescript activities. It's important to remember that the two objects involved have different roles: the requester leads with a request and arguments, the responder follows with execution and a result. If the two objects don't follow the right steps, someone may object and the dance is off.

Operation Expressions

A requester calls an operation on a responder using an operation expression, a type of request operation that requests an operation. An operation expression is typically part of a statement in one of the requester's methods. For a single operation, an operation expression uses this syntax:

<object>.<operationID>(<optional arguments>)
The expression specifies a responder object, then follows it with a period and the identifier of the operation it calls on the object. The operation identifier is always followed with a pair of parentheses that enclose any required arguments. If there is more than one argument, the arguments are separated by commas. If there are no arguments, the parentheses enclose nothing. (If you don't follow the operation ID with parentheses, the compiler assumes that you're getting an attribute, not calling an operation.)

An operation expression always evaluates to the result of the operation. If there is no result, the expression has no value.

Look at a couple of examples to get a feel for the syntax. The expression below creates an integer object with the value 41 and then calls the operation negate on the object. The operation requires no arguments, so the parentheses are empty.

41.negate()
The operation returns the integer -41, so the expression evaluates to -41.

The next operation expression (seen below) calls the operation calculateInterest on an object referred to by the variable mortgage. The operation takes two arguments; both are enclosed in parentheses and separated by commas.

mortgage.calculateInterest(250000, 7.25)
The result of the operation depends on how the operation (a custom operation made up for this example) is defined.

Reading Operation Signatures

Before you create an operation expression and use it in a statement, you must know the operation's identifier, the arguments required, the result returned, and any possible exceptions it might throw. (An exception is an object that signifies an error in execution. You'll find more details about exceptions in Chapter 7.)

If the object is a member of a Telescript built-in class, you'll find its operations described in the class descriptions contained in Part Four of the Telescript Language Reference. (In the future, this information will be in a volume of a different name.) If the object is a member of one of your own custom classes, you can look at the class definition in your source code to see how the operations are defined. If the object is a member of one of someone else's custom classes, you may find the class definition in one of their include files.

The operation definitions you find in the Telescript Language Reference each start with an operation signature that shows you the operation's interface. We'll take a brief look at operation signatures now so you can understand the interfaces of Telescript predefined class operations well enough to use them. This brief look skips the full details of operation signatures; you'll learn them in Chapter <<<yet to be written>>> of the next section--they're part of the information you need to define your own classes.

Operation Signatures

When you look at a class description in the class reference documentation, you'll find the class's operations listed. They are often separated into working groups so you can quickly find the kind of operation you're looking for. If, for example, you look up the Telescript class Number, you'll find operations separated into three groups: conversion operations, binary operations, and unary operations.

The description of each operation starts with an operation signature. Take a look at the operation signature of a custom operation made up for this example:

fix: private op (posX: Number; posY: Number) Boolean throws OutOfBounds;
The first word of any operation definition is the operation identifier you use to call the operation--fix in this case. It's followed by a colon and any optional keywords that restrict use of the operation. In this example, the keyword private means that this operation is private and can only be called by the object itself. The keyword op is present in all operation signatures to signify that this is an operation we're defining, and it's always followed by a pair of parentheses (). The parentheses contain any arguments specified for the operation.

The class name that follows the parentheses (Boolean in this example) specifies the class of the object returned by the operation. If the keyword throws is present, the operation may throw an exception if there's a problem in execution. The class name following throws (OutOfBounds in this example) tells what class of exception may be thrown.

Arguments and Constraints

Each argument within the parentheses has its own identifier followed by a colon and a constraint. A constraint specifies the type of object that will satisfy the argument and, optionally, the passage of the argument--the way the object is passed from the requester to the responder.

The type of the constraint is a class name that specifies what class (or classes) of object will satisfy the argument. In the previous example, the arguments posX and posY each have the type Number. This means that any object that is a member of Number will satisfy the argument--in other words, an instance of Integer or an instance of Real will satisfy the argument.

If the type is followed by an exclamation point (!), then only an object that is an instance of the specified class, not simply a member of the class, will satisfy the argument. Consider this example:

fix: private op (posX: Number!; posY: Number!) Boolean throws OutOfBounds;
Because the arguments above are constrained by Number!, only an instance of Number will satisfy the argument. This means you're in trouble: Number is an abstract class. You can't create an instance of Number, so you can't satisfy the arguments. If you were the programmer who created this operation, you'd be very unpopular with other programmers trying to use the operation.

If the specified class is followed by |Nil, then the argument can be satisfied with any object that satisfies the specified class or by a nil (an object that expresses the absence of a value). For example,

fix: private op (posX: Number|Nil; posY: Number|Nil) Boolean throws
	OutOfBounds;
says that the two arguments can be satisfied with members of Number or by nils. The nil option allows the requester to supply no value for an argument if desired. In other words, a requester could call fix on a responder and leave the parentheses following the operation ID empty. The engine, finding no arguments supplied for the operation, passes a nil for each argument specified for the operation.

The passage part of a constraint is specified by a keyword that can optionally appear before the type specification. The passage keywords are ref, protected, unprotected, copied, and owned. Passage is an advanced topic left for later sections of this book. For now, it's enough to know that if there is no passage keyword in a constraint, the argument's or result's object is passed by reference from the requester to the responder.

If you have more than one argument and constraint in an operation, the constraint of one argument is separated from the following argument with a semicolon (;). In the last example, you saw two arguments: positionX and positionY. Each argument is constrained to the type Number|Nil, and the two arguments with their constraints are separated by a semicolon.

Some operation signatures use two or more successive arguments that have the same constraint, just as fix does in our last example. They don't need to use more than a single constraint in that case; they can combine the argument identifiers, separated by commas, before the single constraint. For example, the previous example could also be defined with this signature:

fix: private op (posX, posY: Number|Nil) Boolean throws OutOfBounds;
This signature still defines two integer arguments, posX and posY.

Return Value

Two optional components follow the parentheses of an operation signature. The first is the constraint of the object returned by the operation. In the previous example, the constraint Boolean means that the operation returns a boolean object: a true or a false. If there is no constraint following the parentheses, the operation returns no result.

The second optional component is the keyword throws followed by the class name (or names) of one or more exception classes. These exception class names advise that the operation may throw any one of these types of exceptions during execution. An exception is an object that--when thrown by an operation--signifies a problem in execution. The operation's requester may catch a thrown exception and deal with it appropriately. If the exception goes uncaught, it can stop execution of the current method. You'll find more detail about exceptions and how to handle them in Chapter 7.

The exceptions listed in an operation signature are advisory only and are put there as a courtesy of the programmer who defined the operation. The operation's methods can throw exceptions that aren't listed here, and the engine itself may throw certain exceptions that are common to all operations--so don't assume that the operation signature shows all possible exceptions that may be thrown.

A Variable Number of Arguments

Some operations may be defined to accept a variable number of arguments. For example, a collection class of some kind may have an operation that adds objects to the collection. That operation could be defined to accept as arguments any number of objects to be added to the collection. The operation signature of such an argument can specify a variable number of arguments, but only as the last element in the list of specified arguments. This signature, for example, specifies a variable number of arguments:

fix: op (posX, posY: Integer; positionReadings: Real ...) Boolean
	throws OutOfBounds;
The last argument in parentheses, positionReadings, has a constraint of Real that is followed by an ellipsis (...). The ellipsis specifies that positionReading may accept zero, one, or more objects as elements of the argument. The constraint limits acceptable values to members of the class Number--in other words, integers or real numbers in this case. If there is no constraint listed, the constraint is Object, so any object can be passed as one of the variable arguments.

This example operation signature requires at least two argument objects, then: one each for posX and posY. It may take three, four, or more argument objects, depending on how many objects the operation requester decides to pass for the variable argument positionReadings.

Specifying a Responder and an Operation

Once you know an operation's signature, you can use a request expression to call the operation on a responder object that offers that operation. As you may recall, an operation expression uses this syntax:

<object>.<operationID>(<optional arguments>)
You may specify the object with a variable (such as mortgage) that refers to the responder, or with an expression that supplies the responder itself (56, an integer object, for example, or MapObject(), an expression that instantiates the MapObject class). You specify the operation using the operation's identifier.

Passing Arguments

The arguments you pass when calling an operation are constrained by the operation signature; you must provide for each argument an object that satisfies the argument's constraint. That object can be represented by any expression that evaluates to the proper object. For example, consider this operation signature:

fix: op (posX, posY: Integer|Nil; positionReadings: Number ...) Boolean
	throws OutOfBounds;
If we assume that each of the responder objects in the following expressions are members of the custom class MapObject (the class that defines the operation fix), some legal expressions calling this operation are:

localMap.fix(546, 1056)
worldMap.fix(latitude, longitude, fieldData1, fieldData2)
areaMap.fix(reading1.round(), reading2.round(), 72.9, 925, 98736.32)
plot.fix(nil, 54)
plot.fix(79)
The first expression supplies two integers for the first two arguments, and no objects for the variable argument positionReadings.

The second expression supplies two integer variables (latitude and longitude) for the first two arguments and two more variables (fieldData1 and fieldData2) that must be numbers--either real or integer--for the variable argument.

The third expression uses operation expressions for the first two arguments--operations within an operation. The round operations each return an integer to satisfy the arguments. The last three arguments passed are a combination of real and integer values that all satisfy the Number constraint of the variable argument.

The fourth expression passes a nil argument, an integer, and no objects for the variable argument.

The fifth expression passes an integer for the first argument, a nil for the second argument, and no objects for the variable argument--just the same as if the expression was

plot.fix(79, nil)
It illustrates an important point about nil arguments: Any trailing nil objects supplied as arguments for an operation may be omitted. The operation automatically fills in nils for those arguments.

Handling a Returned Object

An operation expression evaluates to the result of the operation--if the operation returns a result. If the operation doesn't return a result, the expression has no value. To save the result, you assign it to a variable. This example declares a variable and assigns a result to it:

locationFixed: = Boolean localMap.fix(546, 1056);
The variable allows you to work with the result in later statements.

You'll learn full details of declaring and assigning variables in Chapter 6.

Getting and Setting an Attribute

You'll remember from previous chapters that an attribute is typically a property with associated operations: a getter (for a read-only attribute) or a getter and a setter (for a read/write attribute). You can use an attribute's getter to read its value and its setter (if it's a read/write attribute) to change its value.

You'll find the attributes of each Telescript predefined class in the class reference documentation. Like operation definitions, each attribute definition starts with a signature that tells you how to deal with the attribute. An attribute's signature is much simpler than an operation signature because the syntax for getting or setting an attribute doesn't use arguments or results. Take a look at the attribute signature for a custom attribute:

numberHits: private readonly Integer throws NotRecorded;
Like an operation signature, it starts with the attribute's identifier, in this case numberHits. It's followed by a colon and optional keywords that may restrict the attribute. These keywords are the same as those used for operations with one addition: readonly, which makes the attribute a read-only attribute. In this example, the two keywords private and readonly make this a read-only attribute that can only be read by the object itself.

Following the keywords is a constraint for the attribute--Integer in this example. The constraint gives the type and passage of the object returned by the getter. It also defines the type and passage of the argument of the attribute's setter (if there is one). The object you supply as the attribute's value must be a member of the type specified by the constraint. The passage--which works the same as in the constraint of an operation argument--is by reference if there is no passage keyword in the constraint.

Following the constraint is an optional exceptions clause, the keyword throws followed by the classes of any exceptions that the attribute might throw when used. This element--like its parallel element in an operation signature--is strictly a courtesy provided by the programmer who defined the attribute. The attribute may throw exceptions not listed here, and the engine can throw exceptions that are common to all attributes--and not listed here.

Getting an Attribute

You get an attribute of an object much as you call an operation on an object, but you don't have to worry about passing any arguments. To get a particular attribute of an object, use an attribute expression with this syntax:

<object>.<attributeID>
You specify the responder object and the attribute ID just as you do the object and the operation ID in an operation expression. Notice that there are no parentheses ending the attribute expression.

An attribute expression evaluates to the object returned by the attribute's getter operation. If you want to keep a reference to that object, assign the expression to a variable as in the following example:

visits: Integer;
visits = showPlace.numberHits;

Setting an Attribute

You can set the value of a read-write attribute using an assignment with this syntax:

<object>.<attributeID> = <value>;
You specify the responder object and the attribute ID just as you do in an attribute expression that gets the attribute's value. You then assign a value to that attribute using an object that satisfies the attribute's constraint. Note that you can only set an attribute in an assignment statement. An attribute expression by itself invokes the getter, not the setter. (You'll find full details on assignment statements in Chapter 6.)

Preparing for a Nil Responder

Some attribute expressions, when evaluated, throw an exception because the responder object specified for the attribute doesn't exist yet and isn't allowed to be nil. In such a case, the responder called is a nil responder, and the feature throws a ResponderNil exception.

If you'd like to use an object's attribute and would like to protect against a ResponderNil exception without setting up a catch block (as described in Chapter 7), you can use nil-responder syntax for the expression. It replaces the period before a feature with a question mark and period (?.) as follows:

<object>?.<attributeID>
When an expression using this syntax evaluates, the engine checks the responder first to see if it's a nil. If so, the engine returns a nil and does not throw an exception. If the responder is not nil , the engine gets the value of the attribute. For example,

visits = showPlace?.numberHits;
will set visits to nil if numberHits is a nil. If numberHits is not nil, this statement sets visits to the attribute value.

One thing to be aware of when you use this nil-responder syntax is that the result you get can be confusing in some cases. If the attribute you get can also be a nil, you won't know whether the nil was returned as the result of the getter or because the responder was nil.

Note that this syntax works only for getting an attribute; it does not work for calling an operation.

Using Multiple Features

When you use an object's features--calling its operations and getting and setting its attributes--you typically use one feature per statement, so you specify the object in each statement as in the following code snippet:

visits = showPlace.numberHits;
reportText = visits.asString();
magnitude = reportText.length;
In this snippet, the first statement reads an integer from the numberHits attribute of showPlace. The second statement calls the operation asString on the integer to convert it to a string, and the third statement gets the length attribute of the string to see how many digits there are in the converted number.

You can, if you want, combine attribute reading and operation calls in a single expression. You've already seen one way to do this: by using an operation expression (or an attribute expression) as an argument for another operation. You can also use two other methods: chained feature requests or cascading feature requests.

Chained Feature Requests

An expression using chained feature requests specifies a single object and then follows it with a series of operation and attribute requests separated by periods (.) as shown in the following syntax:

<object>.<featureRequest>.<featureRequest>.<featureRequest>.<featureRequest>
The number of requests following the specified object can be one or more--as many as you care to add. Note that a feature request can be an attribute request or an operation request. If it's an operation request, it must include a pair of parentheses that enclose arguments if there are any.

When the Telescript engine executes a set of chained feature requests in an expression, it calls the first request listed on the specified object. It then calls the second request on the result of the first request, the third request on the result of the second request, and so on until it runs out of chained feature requests.

To see how this works, look at the following chained requests in a single expression whose end result is assigned to the variable magnitude:

magnitude = showPlace.numberHits.asString().length;
This single statement does the same thing as these three statements:

visits = showPlace.numberHits;
reportText = visits.asString();
magnitude = reportText.length;
It's important to remember that only the first of the chained requests works on the first object specified in the expression--the rest of the requests work on the result of the request immediately preceding them. This means that in order to successfully chain feature requests, you must make sure that each request in the chain returns a result upon which the next request will work. In the last example, the attribute numberHits of showPlace returns an integer on which the operation asString is called, returning a string on which length is called, which returns an integer that is assigned to the variable magnitude.

Cascading Feature Requests

If you want to call a series of feature requests on a single specified object, you can cascade feature requests in an expression. The syntax of an expression using cascading requests looks much like that of chained requests, but uses &. between requests instead of a single period. This is the syntax:

<object>.<featureRequest>&.<featureRequest>&.<featureRequest>&.<featureRequest>
The number of cascading requests can be one, two, three, or as many more as you care to add to the expression. Notice that the first request is separated from the object by a period (.), not an ampersand and period (&.).

While cascading requests may look similar to chained requests, the effect is quite different. Each cascading request is called on the specified object at the beginning of the expression, not on the results of the previous request. This statement, for example

79.class&.negate()&.asOctet();
has the same effect as these three statements

79.class;
79.negate();
79.asOctet();
with a couple of important differences. The first difference is that the single cascading expression refers to the object 79 only once, while the set of three expressions refers to 79 three times. Because of this single reference, cascading expressions are often slightly more efficient in execution than single request expressions.

The second difference is that the cascading expression evaluates to only a single value--the value returned by the last feature. In this example, it evaluates to 4F. The results of the first two cascading features--Integer and -79--are dropped. The three expression statements, on the other hand, each return a value for their single feature. This characteristic makes cascading feature requests most appropriate for calling operations that have no results, or that have results that you won't use. In other words, any cascaded features before the last feature should be there only for their side effects--something their method does such as launching an agent or writing values to a dictionary, for example--and not for their returned values.

When Responder and Requester Are the Same Object

There are many times when an object needs to use one of its own features. One of its methods, for example, may want to execute another of its methods in much the same way that a C program uses one of its own functions. In such a case, the requester and the responder are the same object. That is, the object calls an operation on itself, or it sets or gets one of its own attributes.

When an object wants to specify itself for a feature request, it uses the keyword self or an asterisk (*) as the object specification. The syntax is this:

self.<featureRequest>
or this:

*.<featureRequest>
Both syntaxes have the same effect. They each call the feature on the object itself. Remember that there are some features--defined as private operations and attributes--that an object can only call on itself. These features may be called using self or *.

Global Variables

When you specify an object with self, you're using a global variable. A global variable is a keyword used to specify an object that plays a specific role in the Telescript engine. When a feature expression uses a global variable to specify a responder, the engine finds the object that currently fills the specified role, and calls the feature on that object. In the case of the global variable self, the engine calls the feature on the currently executing object (in other words, on the object itself). Note that the symbol * is a compiler synonym for the global variable self.

Other global variables specify objects in different roles. The global variable place, for example, specifies the place object where the requester is located. And client specifies the object that is calling an operation; you can use it in an operation's method to tell what object is calling the operation. Many global variables specify objects in advanced roles that aren't discussed until the section on processes, so we won't go into detail here. You'll find full global variable details in Chapter <<<yet to be written>>>. It's enough to know for know that global variables specify a responder that may be one object in one context and a different object in another context.

Escalating a Feature

Recall from Chapter 2 that a subclass can be defined to override the methods of its superclasses. In other words, even though the subclass inherits the operations of its superclasses, it can create new methods for any of the operations that aren't sealed. This means that an operation called on an instance of the subclass executes in a different manner than the same operation called on an instance of the superclass.

It's possible that an operation can have a different method in each of several generations of subclasses. For example, consider a custom class, WorldMap. It has an operation reckonDistance that calculates the distance between two points in kilometers. The method that calculates the distance is based on information in a large-scale atlas, so its precision is limited to increments of ten kilometers. It can return values such as 520 kilometers, 3780 kilometers, 20 kilometers, and so on.

Now consider a mix-in class, Topo, that provides--among other things--a new method for reckonDistance. The new method uses topographical information from the large-scale atlas to consider changes in elevation over distance. This allows it to return a distance that includes up-and-down travel necessary to go the distance. Because this mix-in contains other operations and attributes that take advantage of topographical data, it's often used to define any new subclass that wants to work with topographical data.

Let's add a third class to this example, a subclass of WorldMap named LocalMap. LocalMap uses the mix-in Topo to add topographical features. LocalMap inherits the method for reckonDistance from WorldMap, but that method is overridden by the reckonDistance method inherited later from the mix-in Topo. Because the programmer creating LocalMap wants better distance resolution than either WorldMap or Topo can provide, he overrides the Topo-inherited method for reckonDistance and provides LocalMap's own method that not only uses topographical data to compute distance, but has much better map data that provides distance resolution up to one kilometer. The result is that reckonDistance is defined with a new method at each level of inheritance: in the superclass WorldMap, in the mix-in Topo, and in the subclass LocalMap.

To help clarify the relationship between these example classes, look at this class tree:

Object

* Map
* * WorldMap
* * * LocalMap(Topo)

Now consider what happens when you call reckonDistance on LocalMap. The operation uses LocalMap's own method to calculate distance, so you get an accurate resolution that takes elevation into account. You may decide that you'd rather not have that accurate resolution because it takes too much time to calculate. Topo's method for reckonDistance would be better for speed. In fact, if you really want speed, you might want to use WorldMap's method for reckonDistance because it calculates low-resolution distance and it ignores elevation.

Escalating an operation allows a responder's operation to use a method from any of an object's immediate superclasses--in this case, Topo or WorldMap. Immediate superclass is an important term here. A class's immediate superclasses are set by the class's definition: they are the flavor class and any mix-in classes specified in the class's inheritance. They are not superclasses of any of those specified classes.

Escalation's limitation to immediate superclasses means that operation escalation can't use methods from super-superclasses. If, for example, WorldMap has its own superclass, Map, and Map has its own method for reckonDistance, LocalMap would not be able to use that method through escalation. Likewise, if Topo had its own mix-in superclass, LocalMap wouldn't be able to escalate to a method from that superclass because it's not an immediate superclass of LocalMap.

It's important to note that an object can only escalate its own operations. One object can't escalate the object of a second object.

Escalation Syntax

The syntax for escalation is:

self::<superclass name>.<featureID>
or it can be:

*::<superclass name>.<featureID>
As you can see, the only difference between an object calling an operation on itself and escalating an operation on itself is the addition of double colons (::) followed by the name of one of the object's immediate superclasses. When an escalation operation expression is evaluated, the engine looks in the definition of the specified superclass for the specified operation and executes the method it finds there. If it doesn't find a method there, it searches the inheritance hierarchy in canonical search order (an activity described in Chapter <<<yet to be written>>> of the next section) until it finds a method to execute.

There's one other variation of escalation syntax:

self::.<featureID>
or

*::.<featureID>
This variation leaves out the superclass specification. When an expression using this syntax is evaluated, the feature is escalated up to the requester's primary superclass--that is, its immediate flavor superclass, not any of its mix-in superclasses (if they exist).

Escalation at Work

Let's go back now to the LocalMap example to see how escalation works. If you have an instance of LocalMap and want, in one of its methods, to reckon a distance in low resolution, you can use this expression to escalate the operation reckonDistance to the immediate mix-in superclass Topo:

self::Topo.reckonDistance(5245, 7465, 98, 432)
When the engine evaluates the expression, it accepts the arguments for the operation. (Remember that the interface--the operation--remains the same wherever it's inherited, so the required arguments remain the same.) It then goes to the class definition for Topo and executes the method associated with the operation reckonDistance. The result is a low-resolution distance that takes elevation into account.

If, from that same instance of LocalMap, you want to reckon a distance in low resolution with no regard for elevation, you can escalate reckonDistance to the immediate flavor superclass WorldMap:

self::.reckonDistance(5245, 7465, 98, 432)
In this expression, the superclass name was omitted for the escalation. The default escalation superclass is the immediate flavor superclass, so reckonDistance is escalated to WorldMap. This expression, which specifies the immediate flavor superclass, would have the same effect:

self::WorldMap.reckonDistance(5245, 7465, 98, 432)
Note that escalation works for attributes as well as operations. Escalating an attribute rarely has any affect, though, because (as you'll recall from Chapter 2) an attribute's getter and setter operations are rarely overridden in subclasses.

Asserting an Object's Type

If you use a global variable such as place or client to specify an object for an operation or attribute, you can't be sure what object the engine will use as the responder. It depends on the state of the engine when the feature is called. You may know, however, that the object will be a member of a certain class, so you can go ahead and call operations and use attributes that are appropriate for that class. You know, for example, that the global variable place will return a place object, so you can call place operations and attributes on the object.

The compiler isn't quite as smart as you are. It checks the features you call on an object against the class of the object, and if it can't find the features as part of the class definition it will return a compiler error. In a case like this, you claim dominance over the compiler by asserting the class of the object--that is, claiming that the object will be a member of a specified class. Assertion syntax is this:

<object>@<class ID>
When an expression using this syntax is compiled, the compiler reads the specified class name and assumes that the specified object is a member of that class. This allows you to call any of the class's features on that object. Consider this expression, for example:

place@MapPlace.mapsAvailable()
The global variable place is asserted to be a member of the custom class MapPlace, so the compiler accepts the mapsAvailable operation call on the place--an operation available only to members of the class MapPlace.

Asserting an object's class may also be useful when you use the object as an argument for an operation. The class of each argument is specified by the operation, and if you don't satisfy the constraint of each argument, the compiler will object vigorously. If you need to, you can assert a class so the compiler won't object. The following operation call shows an example:

kentucky.reckonDistance(0, 0, point2x@Integer, point2y@Integer)
The assertion for the final two arguments of the operation asserts that the variables point2x and point2y are members of the class Integer, which is what the arguments are constrained to be. This is useful if you declared the variables for class Number, but know that at this point in your code they will carry integers.

Be aware that improperly asserting an object's class can wreak havoc. In other words, make sure that you're really smarter than the compiler before you assert an object's class. The compiler will catch egregious assertions--declaring a variable to be Integer, for example, and then asserting that it's Real in a later statement--but it won't catch everything.

Improper assertions that pass the compiler may backfire on you when the engine interprets your code. The engine checks the object again and, if the object you asserted isn't truly what you asserted it to be, the engine throws an exception. Note that in the current release of Telescript the engine does not check types in all cases (properties and variables, for example), so programmer beware.

* * *

Now that you know how to use an object's features, you can go on to the next chapter to read about some real Telescript classes and the features they offer: the primitive classes, which include Integer, Real, Character, and other useful classes.

4 - Using an Object's Features
Calling an Operation
Operation Expressions
Reading Operation Signatures
Operation Signatures
Arguments and Constraints
Return Value
A Variable Number of Arguments
Specifying a Responder and an Operation
Passing Arguments
Handling a Returned Object
Getting and Setting an Attribute
Getting an Attribute
Setting an Attribute
Preparing for a Nil Responder
Using Multiple Features
Chained Feature Requests
Cascading Feature Requests
When Responder and Requester Are the Same Object
Global Variables
Escalating a Feature
Escalation Syntax
Escalation at Work
Asserting an Object's Type

TS Ref - 26 JUN 1996

Generated with Harlequin WebMaker