Helpers
Here is a sample script for an object which is helped by two other objects, named Rex and Sue:
sayHello -- let's start out by being friendly!
properties
helpers: [Rex, Sue],
birthdate: "May 14, 1942",
end properties
on sayHello
put "Greetings! My age is " & calculateAge()
end sayHello
This object’s sayHello handler calls a function named calculateAge. When the sayHello handler is called, it will call calculateAge(). This object doesn’t have a calculateAge function handler, but if one of its helpers, say Rex, does have such a function handler, that handler will be run on behalf of this object, to calculate the age.
Who is Me? What is This Object?
An object and its helpers work closely together, acting very much like a single object (think of the Three Musketeers’ slogan, "All for one, one for all"). When a handler in a helper is being run, it is treated as though it were a handler of the original object. In this context, me and my refer to the object being helped rather than the helper.
This means that any statements executed in the helper’s script will target their messages to the object being helped (and only indirectly, then, to the helper). It also means that if the helper uses me or my to access properties, it will be accessing the properties of the helped object. For cases where a script may need to target itself or access its own properties, the term this object may be used instead of me.
Helpers’ Place in the Message Path
An object’s helpers are also closely tied to it in the message path. When an object receives a message that it doesn’t have a handler for (or which it handles and then passes along), that message will go next to the object’s helpers. Only if none of the helpers handle the message will it be passed along to other objects in the message path, such as to those in the backScripts.
Objects Designed to be Helpers
Any object can be a helper to one or more other objects. Suppose, for example, you have an object called Rex that has a calculateAge() function handler. Then suppose you have another object, Luna, that needs the same calculateAge() functionality. By adding Rex to Luna’s list of helpers, Luna gains that capability. But Rex may have other handlers as well, which may not all be desirable to include as part of Luna’s behaviors. How can you provide just the functionality that Luna needs?
One good way to deal with this situation is to separate out those behaviors (handlers) that may be useful to several different objects, and create a new object designed specifically to serve as a helper. For example, you might move the calculateAge() handler out of Rex into a Person object, and then add Person to both Rex and Luna’s helper lists. Each object can have any number of helpers, so separating related groups of behaviors out into helper objects can be a very powerful tool, allowing you to re-use that functionality in various combinations in different objects.
Designing an Object to be a Helper
Many helper objects are quite simple, having nothing more than a script with a few handlers. A Person object, for example, might start off with just a single handler, like the calculateAge() function that we’ve been talking about:
to calculateAge -- returns my current age in years
return (the date - my birthDate) / 365.25 days
end calculateAge
More on Me (oh My!)
One very important thing to understand about helpers is that when the terms me and my are used in the script of a helper, they refer to the object being helped and its properties, not to the helper or its properties (use this object if necessary to refer to the helper). This usually means that a handler written as part of an object will also perform correctly when helping another object, without any changes.
Making Objects Based on Other Objects
Simply using one object as a helper to another in order to borrow the first object’s behavior is very easy, often requiring no special effort on your part. Once you’ve decided to separate some of the functionality of your scripts into an object that will serve mainly as a helper to other objects, there are some additional capabilities you may want to take advantage of.
Earlier in this section, we saw how a simple object could be created using a new object expression. It was mentioned that the more common way to use a new object expression is to indicate a particular type of object that you want to create by providing the object it should be based on, as shown in this example:
put new Person with (name:"Elizabeth", age:14) into daughter
Here, Person is the name of an object (known as a "prototype" object) being used to define the newly-created object. In this example SenseTalk will look for an object named "Person" and send that object a makeNewObject function message with the initial properties supplied as a parameter. This gives the prototype object the chance to control exactly what the new object will be like. Typically, the new object will be based on the prototype object itself, by having the prototype object as its helper, thus inheriting all of its behaviors, and by receiving a copy of the prototype's regular properties (other than its script and helpers). Let’s look more closely at exactly how this works.
The Role of a Prototype Object
If a prototype object has a makeNewObject function, that function should create and return a new object. If it doesn’t handle the makeNewObject message, SenseTalk’s built-in makeNewObject function will be called, which will create a new object helped by the prototype object (Person, in our example), set any properties of that object that were supplied in the new expression ("name" and "age" in this case), add any properties from the prototype that aren't already present, and then send the object an "initialize" message. So, the effect of the built in behavior is essentially the same as this makeNewObject function:
to makeNewObject initialProperties
get new object with initialProperties helped by me
add properties of me to it
send "initialize" to it
return it
end makeNewObject
If a different behavior is needed, simply write a custom makeNewObject function in your prototype object. You might, for instance, want to assign some default initial property values, include additional helpers, or perhaps even call on a different prototype object to make the new object.
Notice that the default behavior is to create a new object that is helped by the prototype object. So any object can be used as a prototype object, and will become the primary helper of new objects created from it.
Initializing the Properties of a New Object
When a new object is created from a prototype, by default it inherits all of the behaviors of the prototype object, and also copies of its properties. It also receives any properties supplied with the new expression (which will override any corresponding property values from the prototype). A prototype object may also provide additional properties for the new object. One way to do this, as already mentioned, would be to write a custom makeNewObject function, although this is rarely needed. A much simpler (and better) choice is to take advantage of the "initialize" message that is sent to the new object by the built-in makeNewObject function (the "initialize" message is sent only to the object and its helpers, not through the full message path). Here is an example initialize handler that provides some default property values:
to initialize
add properties {time:"12:00", priority:"Normal"} to me
end initialize
If this handler is in a prototype Appointment object that has a month property with a value of "May", then a new appointment can be created like this:
put a new Appointment with {time:"8:30"} into meeting
This will create a new object helped by Appointment, with its time property set to "8:30" and its priority set to "Normal". Because the add properties command doesn’t replace any properties that are already present, any values supplied with the new expression will take priority over those provided by this initialize handler. The new object will also have a month property with the value "May" that was copied from the prototype object.
Creating Empty Objects
Sometimes you may want to create a new object that doesn't automatically receive copies of all of the properties from the prototype. To do this, use the word empty before the prototype:
put a new empty Appointment with (time:"8:30") into meeting
Here, the new object will be helped by Appointment, with its time property set to "8:30", and its priority property set to "Normal" (from the initialize handler). It will not receive the month property (or any other properties) copied directly from the prototype object.
Displaying Objects as Text
When an object is displayed as text, by default its keys and values are displayed in a format determined by the plistPrefix, plistSuffix, plistKeySeparator, plistEntrySeparator properties, as described in Lists and Property Lists. However, objects can represent themselves as text strings in any way they like, by implementing a handler for an asText function as part of their script, like this:
to handle asText
return "Part number " & my partNum
end asText
If an object does not handle the asText message, but does have an asText property, the value of that property will be used as the string representation of the object. This is very convenient for some simple objects that may not have a script at all.
For more flexibility, if an object has neither an asText handler nor an asText property, but it has an asTextFormat property, the value of that property is evaluated by the merge() function to provide the text representation of the object. This provides a more dynamic solution that can combine the values of other properties of the object:
set account to {type:"Savings", balance:1234.25,
asTextFormat:"[[my type]] Account: [[my balance]]"}
put account -- "Savings Account: 1234.25"
add 5050.50 to account's balance
put account -- "Savings Account: 6284.75"
Checking Object Contents
When a script checks whether or not an object contains a particular value using either the contains or is in operators, the object (or one of its helpers) may implement a function to perform the test in any way it wants. When either of these operators is used to check for the presence of a value in an object or property list, a containsItem function message is sent to that object. If it implements a handler for this message, that handler will be passed two parameters – the value to search for, and a boolean indicating whether the test should be case-sensitive or not – and it should return true or false to indicate whether the indicated value is present in the object or not.
If an object does not handle the containsItem message, SenseTalk's implementation of that function will be called. The behavior of the built-in function can be configured by setting the objectContainsItemDefinition global property. If this property is set to AsTextContains (the default behavior) the containsItem function returns true if the object's text value (as described above) contains the search value. When set to NestedValueContains, the operator is applied to each of the object's values and the expression will yield true if any them contains the search value. Finally, a setting of KeyOrValueEquals will return true if the search value is equal to one of the object's keys or to one of its values.
Advanced Topic: Early Helpers
An object’s helpers provide additional behavior by potentially handling any messages sent to the object that it doesn’t already handle on its own. The object can override any handlers provided by its helpers by supplying its own handlers. Occasionally, it may be useful to be able to work the other way around, providing an object with a helper that can override behaviors defined by the object itself. Early Helpers do this. Any messages received by an object are sent first to the object’s early helpers, then to the object itself, and finally to its helpers.
Assigning Helpers to an Object
We have already seen a number of ways that helpers can be assigned to an object when it is first created: by including a list of helpers in a properties declaration of a script file; by listing them in a "helped by" clause following a property list or a new object expression; or by creating an object from a prototype object that becomes its first helper.
An object’s list of helpers—and its list of early helpers—can also be modified dynamically at any time by a script. Simply access the helpers or early helpers property and insert or delete objects as needed:
insert CleverTricks into the helpers of Fritz