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.
<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.
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.
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.
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
.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.
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
.
<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.
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.
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.
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.
<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;
<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.)
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.
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.
<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
.&.
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 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 *
.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.
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)
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.
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).
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.
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.
Integer
, Real
, Character
, and other useful classes.
Generated with Harlequin WebMaker