Object-Oriented Scripting in Mathscript

In this section, we discuss the Mathscript primitives that implement FLAVORS, a lisp style of object-oriented programming. Mathscript gives the author opportunities to explore and use this paradigm. The Mathscript Commands and Functions that are relevant here are:

Get

Send

Type

Object

Method

What is object-oriented programming? It is a highly disciplined and structured style of programming that is modular, easily extensible, and well adapted to conceptual, top-down design.

In Mathscript, we may define "Types" of objects. For example,

we might have a taxonomy like:

type: Movable_objects

  |
  |
  _________________________________

  | |
  | |

type: Boats type: Vehicles
  | |
________________ _____________________

  | | | |
  | | | |

type: Rowboats type: Yachts type: Trucks type: Cars

  |
  |

  type: Pickups

We define these abstract types, and the "tree" of subtype relations with the Command "Type".

Once we have determined the hierarchy of Types of objects, we may define particular "objects" in these types using the Object Command. In the type VEHICLES, we might define objects: Bus_#12, and Orient_Express. In the subtype CARS, we might define object: Yellowcab_#147. In the type of Pickups, we might define objects: Jimmys_Dodge, and Chevy_VAN_#31.

The main protocol in object-oriented program design is the notion of a message. Individual objects maintain private (local) variables that determine their "states". Any object can send a message to any other object that can have one of three effects:

1) It might be a "query" asking for the value of a local variable

2) It might be a request for the receiver to change the value of a variable

3) It might be a request for the receiver to do something using the receiver's "method" for doing it.

Each type of object has defined for it the names of local (instance) variables that every member of that type will have access to.

For example, we might define, for every movable object, variables:

POSITION, VELOCITY, FUEL_SUPPLY.

Each object type such as: MOVABLE_OBJECT will have the list of names of the local variables that its objects can have on the "Varlist" property of the type name. This will be put there when the type is defined using Type. Each instance object of the type when it is defined will assign initial values to each of these variables. These will be stored as a property list under the "Instvar" property of the instance object.

Now when a subtype of objects (such as BOATS) is defined, it must be given its own list of local variables, and so on. It is usually given a list of local variable names and values that are unique to the subtype. The subtype inherits the local variables already defined for any supertype. For this reason, the object hierarchy should be defined from the top down, with Types defined first for the most general types, then for the more specific, and with Objects defined last.

In this way, every object instance of each type defined has a set of local variables that defines its state. The name of each instance object evaluates to itself. These instance objects are defined using the Command "Object".

If an object, say Queen_mary, belongs to the type BOAT, and if it has a local variable called: speed, then we, or any object can inquire about the value of that variable by sending it a message in the following way (using the Get Function ):

make foo Get (queen_mary, speed);

If we, or an object want to change the value of that variable, we send it a message in the following form (using the Send Command):

send queen_mary putspeed 30

This is the way that requests for values, or to change values are handled.

The function Get, and the Command Send handle all message passing. The methods: speed and putspeed are automatically defined when the variable Speed for the type is defined in the Type Command.

Now the most interesting type of message is the third type. Whenever a

type is defined, certain "methods" for handling message requests are defined

for the entire type. Each instance object of the type has access to the single piece of code defining the method.

A "method" is a name that is associated with the name of a MathScript Program. It may take arguments or not. We might define for the type MOVABLE_OBJECTS the method:

update with arguments: current position, velocity, interval.

We do this in two steps, using the Method Command in the following way:

First, a program is defined that the method will use. Define the MathScript Program, update_position, using the Program Protocol.

Program update_position (ex c) (ex v) (ex i) {<body of program>};

Next, the method is declared and associated to the program. It must be done in this order.

Method Movable_objects update update_position

This creates a method called update, and associates it with the already defined program called update_position.

Evaluating:

method <type> <method_name> <program_name>

where <type> evaluates to the name of the type,

<method_name> evaluates to the name of the method

<program_name> evaluates to the name of a program

defines the method for any instance of the type, or of any of its subtypes.

Then a message to a particular object of the form:

send queen_mary update current_position, velocity, interval

(the commas are necessary) would cause:

update_position(current_position, velocity, interval)

to be evaluated. The actual arguments for update_position will be:

This is discussed in general in the Send command.

Finally, a subtype of MOVABLE_OBJECT, such as BOAT might define its

own method for handling "update". We can arrange this, for example, by evaluating:

method boat update boat_position.

where boat_position is the name of a program different from update_position that works

for boats.

When an object gets a message to apply a method, it first checks to see if it has

the method at its own level in the hierarchy. If it does, it applies it. Otherwise, it

searches its "ancestor types" in depth-first fashion to see where it first inherits the method. Then it applies the method it first finds.

Now messages will often cause new messages to be sent. These are evaluated in a fully recursive way.

The special names: self and me

Often, in the evaluation of a method, an object must send a message to itself, for example to read one of its variables, or to change it. Since methods are defined for "types", and not for particular "instance objects", this is not as easy to arrange as may seem. It is not generally known in advance which particular instance object is going to receive the message that tells it to send itself a message, so the message cannot name the receiver. The way around this is to use the special name: Self.

If an object receives a message that causes another message of the form:

send self <message>

to be evaluated, then that object will send the message to itself. Similarly, if an object receives a message that causes another message

send me <message>

to be evaluated, then it will send that message to the object that sent it the original message.

New objects, types, and methods can be defined dynamically at run

time in this implementation of object-oriented programming.

We shall specify the basic object programming functions shortly. They

are all implemented for speed and efficiency in assembly language as primitive MathScript Commands and functions.

We now list the 4 basic Commands, and the Single Function for object programming in MathScript. These are loosely modelled on ZetaLISP "Flavors" protocol. All of these functions

are implemented as primitive (assembly language) routines.

Type

Number of Arguments: 3

Argument types: first is a variable, last two are lists

Description: This is used to define an object type.

It is has the form:

type <type_name> <vars> <ancestors>

<type_name> is a variable, the name of the new type.

<vars> evaluates to a list, the list of the "instance variable names

of objects in this type. The instance variables of ancestor

types are automatically inherited, and their names need

not be listed.

<ancestors> evaluates to a list, the list names of of immediate ancestors

(supertypes) of this type. It may be empty: list().

The <vars> are associated to the type name as the VARLIST property.

The accessor functions are created for the new variables at this time.

The effect is to create the full list of instance variables for this type, including variables associated with all ancestor types. When a message is sent to an object of this type to access one of the ancestor variables, the access is made by virtue of the fact that this is a subtype of an ancestor type that has those access functions. Thus the access functions are only created once, at the highest level of the hierarchy for which they will be used. This is an important consideration if space is at a premium.

Method

Number of Arguments: 3

Argument types: The first 2 are variables, the third is a program name

Description: This is used to define methods for object types.

The general protocol is:

First, define a program with name program_name that actually implements the method.

program <program_name> <args> { <body> }

Then, declare the method itself.

method <type_name> <method_name> <function_name>

<type_name> evaluates to the name of the type for which this method is being defined.

<method_name> evaluates to the name of the method being defined.

<function_name> evaluates to the name of a MathScript Program, defined

first, that actually implements the method.

The code for this method appears once, at the highest type level for

which the method will be used. Thus, define the method once. Subtypes will automatically access it.

If you want to give a subtype a special method (with the same name as its ancestor method), it is permissible. Sometimes it is necessary. Simply associate the method name with the special program in the Method Statement. The subtype will automatically use its own (local) method program if it finds one.

The advantage of this strategy for the creation of modular and easily modifiable code is obvious. Methods appear once (usually) and thus need only be changed once in place for all objects that might use them.

Object

Number of Arguments: 3

Argument types: first two are variables, last is a list

Description: This is used to define actual instance objects for

given types.

The protocol is:

object <name> <type> <initializations>

<name> evaluates to a variable, the name of the object.

<type> evaluates to a variable, the name of the type to which it belongs.

<initializations> evaluates to the ordered list of instance variable names followed

by values in the form: list(var1, value1, var2, value2 .vark, valuek).

This is an initialization list.

At creation time, all instance variables are automatically initialized to FALSE. This list overrides that default initialization for the instance variables listed. The full list of variables/values is stored as the INSTVAR property of the object. It is stored as a property list:

Send

Number of Arguments: indeterminate

Argument types: first two are variables, rest are arbitrary types, separated by commas

Description: This is used to send messages to objects.

The protocol is:

send <object_name> <method_name> arg1, arg2, ... ,argk

<object_name> evaluates to a variable, the name of the receiving object.

<method_name> evaluates to a variable, the name of the method.

The number of remaining arguments depends on the number of arguments that the program associated with the method name takes. If the program takes no arguments, you must supply the single argument: FALSE. Otherwise, the number you supply must be the same as the number the program expects, in the correct order. These arguments must be separated by commas.

Now there are 3 types of methods.

If the receiving object has a instance variable of name VAR, then the method of name VAR simply accesses its value. The Send Command cannot return a value, and so it cannot be used for access. Instead, the Get Function is used, and it will be discussed next.

If the message has the form:

send object1 putvar val

then object1 resets the value of VAR to

value[val]. The accessor methods: VAR and PUTVAR are created when

the Type Command first defines the type.

The third type of method is the kind created using the method command. The method command names an ordinary MathScript program to implement the method. That MathScript program may take arguments. Those arguments are supplied as arg1, arg2, ..., argk in the call above.

Get

Number of Arguments: 2

Argument types: Variables

Function Description: This is used to read objects' instance variable values.

The protocol is:

Get ( <object_name> , <variable_name> )

<object_name> evaluates to a variable, the name of the receiving object.

<variable_name> evaluates to a variable, the name of the variable.

If the receiving object has a instance variable of name VAR, then the method of name VAR simply accesses its value. The Get Function sends a message to the object, and returns its value. Thus:

Get( Object1, var ) returns the var value of object1.

The evaluation of messages is fully recursive, as one would expect in MathScript.

Notice that messages may be sent from one MathScript package to another via

the Export Command. Thus colloquience works with objects to promote the

creation of modular program designs on a large scale.

Having defined the protocol for object oriented programming, we give

an illustration with a little program called CARS.

CARS: An object oriented program

When CARS is loaded in, 3 flavors (types) are defined:

type: FACTORY

|

|

________________________

| |

| |

type: PROD type: TRUCK

FACTORY is the type of all storage sites of car parts

PROD is a type of manufacturers, or producers of cars

TRUCK is a type of mobile storage sites of car parts (trucks)

Each of these has instance variables: wheels, windows, and chassis whose values are the number on hand. Additionally, the objects of type TRUCK have instance variables: location (the name of a fixed storage site) and left: (a list of storage sites it has yet to visit)

A car requires 4 Wheels, 6 Windows, and 1 chassis

Initially, there is an object of type PROD that manufactures cars. It

is called PRODUCER and has 9 wheels, 13 windows, and 2 chassis.

There are three objects of type FACTORY: FAC1, FAC2, and FAC3. Think of them as warehouses.

FAC1 has 7 wheels, 6 windows, and 0 chassis

FAC2 has 0 wheels, 10 windows, and 3 chassis

FAC3 has 12 wheels, 13 windows, and 2 chassis

The single object MYTRUCK (of type TRUCK) has no parts, is located at the PRODUCER, and has yet to visit FAC1, FAC2, and FAC3.

The truck has two methods: Dispatch, and Unload. All objects of type FACT have methods: Ship and Loadit

The PRODUCER has methods: Order, and Build

  1. send Mytruck dispatch <destination>

causes the truck to go to <destination>, updating its location, removing that location from its Left list, and printing a message that it's on its way.

  1. send Mytruck unload FALSE

causes the truck to remove all of its cargo, and turn it over to PRODUCER (its current location). It updates PRODUCER's variables, and prints the message that it is

unloading. Then it sets its LEFT list to (fac1 fac2 fac3), preparing for a new round.

  1. send storage_site ship cargo, <destination>

can have several effects.

- if the truck is at the storage site, then the storage-site sends itself the message to Loadit the cargo on the truck for the destination

- otherwise, if the truck is at the PRODUCER, the storage site first Dispatches the truck to itself, then Ships the cargo to the destination

- otherwise, the truck is at one of the warehouses. The storage_site sends that warehouse the message to Ship its cargo to it. The effect of that will be for the site at the truck location to Load its cargo for the calling storage site.

4) send storage_site loadit cargo, destination

has the following effect:

The truck is assumed to be at the storage site.

The storage site computes what's needed (cargo) and looks at what it has. It supplies as much as it can in each category: wheels, windows, and chassis. It updates its variables, the truck's variables, and recomputes the remaining deficit (new cargo).

If there is no deficit, cargo is (0,0,0) then it Dispatches the truck to the producer. Otherwise, if the destination is not the producer, it dispatches the truck to the destination, and sends the destination storage-site the message to Ship the (new) cargo to the producer. Otherwise, if the destination is the producer, then it sends the truck to the next site on its Left list, if there is one left to visit, and sends that site the message to Ship the (new) cargo to

the producer. If there are no sites left to visit, it Dispatches the truck with what it has to the producer.

5) send producer build n

causes the producer to compute the number of parts it has, and to build as many cars as it can with that number. It updates its records, and prints the number of cars actually built.

6) send producer order n

causes the producer to look at the stock on hand. If it has enough, it sends itself the message to Build n cars. If it doesn't have enough, it sends FAC1 the message to Ship a cargo consisting of the missing parts to it. Then it sends itself the message to BUILD n cars.

With that said, the following is a scenario with the original data:

send producer order 5

Producer requests 11 WHEELS 17 WINDOWS 3 CHASSIS from first factory

truck is going to FAC1

FAC1 is loading truck with 7 WHEELS 6 WINDOWS 0 CHASSIS

truck is going to FAC2

FAC2 is loading truck with 0 WHEELS 10 WINDOWS 3 CHASSIS

truck is going to FAC3

FAC3 is loading truck with 4 WHEELS 1 WINDOWS 0 CHASSIS

truck is going to PRODUCER

truck unloading 11 WHEELS 17 WINDOWS 3 CHASSIS

BUILDING 5 CARS

send producer order 3

Producer requests 12 WHEELS 18 WINDOWS 3 CHASSIS from first factory

truck is going to FAC1

FAC1 is loading truck with 0 WHEELS 0 WINDOWS 0 CHASSIS

truck is going to FAC2

FAC2 is loading truck with 0 WHEELS 0 WINDOWS 0 CHASSIS

truck is going to FAC3

FAC3 is loading truck with 8 WHEELS 12 WINDOWS 2 CHASSIS

truck is going to PRODUCER

truck unloading 8 WHEELS 12 WINDOWS 2 CHASSIS

BUILDING 2 CARS

Now with all of that said, the following is the code for the CARS program. It would be a good idea to study it for the structure of the various Methods that we discussed. The code was written to send its output to some selected textfield. We actually create two scripts: cars.msp and cars2.msp because the compiler puts a limit of 8192 bytes on the size of individual files that it compiles. We simply load "cars2" at the end of the cars script to get around this problem.

Enter it with whatever initial conditions you like, and you will see the code "simulating" the protocol described above, the objects keeping track of their own states.

Cars.msp

make variable factory;

make variable prod;

make variable Truck;

make variable producer;

make variable fac1;

make variable fac2;

make variable fac3;

make variable MyTruck;

type factory list(wheels, windows, chassis) list();

type prod list() list(factory);

type Truck list(location, left) list(factory);

object producer prod list(wheels, 9, windows, 13, chassis, 2);

object fac1 factory list(wheels, 7, windows, 6, chassis, 0);

object fac2 factory list(wheels, 0, windows, 10, chassis, 3);

object fac3 factory list(wheels, 12, windows, 13, chassis, 2);

object MyTruck Truck list(wheels, 0, windows, 0, chassis, 0, location, producer, left, list(fac1, fac2, fac3));

program min (ex ee) (ex ff) {

if ee < ff then {return ee} else {return ff};

}

program max (ex ee) (ex ff) {

if ee < ff then {return ff} else {return ee};

}

program dodispatch (ex dest) {

send self putlocation dest;

send self putleft delete (dest, get( self, left));

printon "MyTruck is going to", dest;

skip;

}

method Truck dispatch dodispatch;

program dounload {

let u be get(self, wheels);

let v be get(self, windows);

let w be get(self, chassis);

send self putwheels 0;

send self putwindows 0;

send self putchassis 0;

send producer putwheels u+get(self, wheels);

send producer putwindows v+get(self, windows);

send producer putchassis w+get(self, chassis);

send self putleft list(fac1, fac2, fac3);

printon "MyTruck unloading", u, "wheels", v, "windows", w, "chassis";

skip;

}

method Truck unload dounload;

program doship (ex cargo) (ex des)

{

let loc be get( MyTruck, location);

if loc == self then {

send self loadit cargo, des;

} else {

if loc == producer then {

send MyTruck dispatch me;

send self ship cargo, des;

} else {send loc ship cargo, me;}

}

}

method factory ship doship;

program doload (ex cargo) (ex dest) {

let l be false;

let m be false;

let a be evaluate(first(cargo));

let b be evaluate(first(rest(cargo)));

let c be evaluate(first(restlist(cargo,2)));

let aa be get(self, wheels);

let bb be get( self, windows);

let cc be get( self, chassis);

printon self, "is loading MyTruck with";

let m be min(a,aa);

printon m, "wheels";

let aa be aa - m;

let a be a - m;

send MyTruck putwheels m + get( self, wheels);

let m be min(b,bb);

printon m, "windows";

let bb be bb - m;

let b be b - m;

send MyTruck putwindows m+get(self, windows);

let m be min(c,cc);

printon m, "chassis";

skip;

let cc be cc - m;

let c be c - m;

send MyTruck putchassis m + get(self, chassis);

send self putwheels aa;

send self putwindows bb;

send self putchassis cc;

let cargo be list( a, b, c);

if a+b+c == 0 then {send MyTruck dispatch producer} else {

if not( dest == producer) then {

send MyTruck dispatch dest;

send dest ship cargo, producer;

} else {

let l be get(MyTruck, left);

if l then {

send MyTruck putleft rest(l);

let l be first(l);

send MyTruck dispatch l;

send l ship cargo, producer;

} else {send MyTruck dispatch producer;}

}

}

}

method factory loadit doload;

load "cars2";

 

The second part of the script is called "cars2" and is saved in a separate file called cars2.msp. This is because the compiler cannot compile the entire cars script at one go.

Cars2.msp

program doorder (ex n) {

let a be get(self, wheels);

let b be get( self, windows);

let c be get( self, chassis);

let aa be max(4*n - a, 0);

let bb be max(6*n - b, 0);

let cc be max(n - c, 0);

if 0 < aa+bb+cc then {

printon "Producer requests", aa, "wheels", bb, "windows", cc, "chassis from first factory";

skip;

send fac1 ship list(aa,bb,cc), producer;

send MyTruck unload FALSE;

send self build n;

} else {send self build n;}

}

method prod order doorder;

program dobuild (ex m) {

let xx be get(self, wheels);

let yy be get( self, windows);

let zz be get( self, chassis);

let x be quotient( xx, 4);

let y be quotient(yy, 6);

let z be zz;

let i be min (m, z);

let j be min (i, y);

let k be min (j, x);

send self putwheels xx - 4*k;

send self putwindows yy - 6*k;

send self putchassis zz - k;

printon "building", k, "cars";

skip;

}

method prod build dobuild;