prev up next

2) Simple objects

The following is the Aldwych code which defines a simple object that just keeps a count from which values may be added and subtracted:

#count(sum)~
{
 add(n) | sum+=n;
 sub(n) | sum-=n;
 val-   |>sum
}
With this definition, A<-count(100) would create a count object, named A, which initially stores 100. Then A.add(n) is an Aldwych statement which causes the value in variable n to be added to the value in the object referred to by A, while A.sub(n) causes the value in variable n to be subtracted from it. A.val->m causes the value in the object referred to by A to be copied into variable m. The semi-colons here are clause separators, so one is not needed at the end of the last rule, but it would not cause an error to put one in as a completely empty rule is ignored.

If the code for count was combined with the following:

#main ==
   Screen<-aio\stdio(),
   Screen.fwrite("Enter initial value: ")
         .readInt->n
         .fwrite("Enter amount to add: ")
         .readInt->a
         .fwrite("Enter amount to subtract: ")
         .readInt->b
         .fwrite("Final amount is: ">+c)
         .nl,
   Acc<-count(n),
   Acc.add(a).sub(b).val->c;
in a file which was compiled as in part 1, the result would be a program which prompts for a number and then creates a count object initially storing that number from which a further number is added, another subtracted and the resulting value in the object printed.

An object descriptor consists of a header, and a set of rules enclosed by braces. Semicolons separate the rules. Each rule has a left-hand side (lhs) and a right-hand side (rhs) separated by a bar.

In this simple form of object descriptor, a header consists of the symbol # followed by a list of arguments enclosed by brackets. The arguments are separated by commas (but in this case there is only one argument) followed by the symbol ~. The arguments coincide with fields for the object and when a new object is constructed each of them is given a value by the construction call.

The lhs of a rule consists of a message name followed optionally by some arguments. If there are arguments they are enclosed by brackets and separated by commas. If the message has a return value, it is followed by the - symbol. If the message returns an object rather than a value, it is followed by a ~ symbol on the lhs of the rule rather than a - symbol.

On the rhs of a rule, a+=b is shorthand for a<-a+b. The values of fields in the object may be changed in this way. Arguments to the message on the lhs may be used as variables on the rhs, but the variables may not be assigned values. If a message expects a return value or object, the rhs must contain a statement consisting of the > symbol followed by an expression which gives the return value.

A field may be given a default value. Here is a version of count in which the sum field has the default value 0:

#count(sum<-0)~
{
 add(n) | sum+=n;
 sub(n) | sum-=n;
 val-   |>sum
}
With this, a statement A<-count() creates a count object whose sum field is initialised to 0. A default may be overridden by an object creation call which contains within it <name><-<value>, where <name> is the name of the field whose default value is to be overwritten, and <value> is the value which overwrites it. Here is a version of the above main which would accomplish the same as the previous one, but deal with the version of count which has a default value for sum:
   Screen<-aio\stdio(),
   Screen.fwrite("Enter initial value: ")
         .readInt->n
         .fwrite("Enter amount to add: ")
         .readInt->a
         .fwrite("Enter amount to subtract: ")
         .readInt->b
         .fwrite("Final amount is: ">+c)
         .nl,
   Acc<-count(sum<-n),
   Acc.add(a).sub(b).val->c;
An object may have fields that are initialised in its own descriptor and whose initialisation may not be overridden. These fields are listed after the ~ in the header, in a list opening with <( and closing with ), with commas as separators. Each of these fields must be given a value by an assignment, the value may be an expression containing other fields. Here is a version of count in which the sum field is initialised to 0 which may not be overwritten:
#count()~
<(sum<-0)
{
 add(n) | sum+=n;
 sub(n) | sum-=n;
 val-   |>sum
}
In fact, if the argument list in an Aldwych header is empty, the brackets can be omitted, so the above could be written:
#count~
<(sum<-0)
{
 add(n) | sum+=n;
 sub(n) | sum-=n;
 val-   |>sum
}

If a value of an argument is needed only to create an object, but is not after that to be used as a field, it should be preceded by a - symbol in the header. For example, the following is an object descriptor for time objects which take hour and minute values when they are created but store their time internally purely in minutes:

#time(-hrs,-mins)~
<(totmins<-hrs*60+mins)
{
 add(n)  | totmins+=n;
 sub(n)  | totmins-=n;
 hours  -|>totmins/60;
 minutes-|>totmins//60;
 toString-|>"">+totmins//60>":">+totmins/60;
 clone~  |>time(0,totmins)
}
Note this is not quite correct, as the toString method which returns the time as a string in "hh:mm" format does not correctly handle cases where the number of minutes is less than ten. To do that requires features covered later. The objects created by this descriptor can take an add(n) message to advance the time they store by n minutes and a sub(n) to take it back by n minutes. They can also take hours messages which return the number of hours in the time they store, rounding down the minutes, and minutes messages, giving the number of minutes past the hour of the time they store.

The clone message which time objects can take returns a copy of the object itself (showing the notation for a message that returns an object). Assignment of object variables creates multiple references to the same object, so A<-B, for example, causes A to refer to the same object that B refers to. Therefore a method such as clone is needed to copy objects; A<-B.clone would cause A to refer to an object that is a copy of that referred to by B, assuming B is of a type that takes a clone message which behaves similarly to clone in time above.

That object assignment causes aliasing could be demonstrated by the following main method combined with time:

#main ==
   Screen<-aio\stdio(),
   Screen.fwrite("Enter initial hours: ")
         .readInt->hrs
         .fwrite("Enter initial minutes: ")
         .readInt->mins
         .fwrite("Enter minutes to add: ")
         .readInt->advance
         .fwrite("Final time (1) is: ">s1)
         .nl
         .fwrite("Final time (2) is: ">s2)
         .nl ,
   Time1<-time(hrs,mins),
   Time2<-Time1,
   Time1.add(advance).toString->s1,
   Time2.toString->s2;
It is possible that both times printed will be the same, since Time1 and Time2 refer to the same object. However, it is not inevitable, due to Aldwych being a concurrent language. It is possible for Time2.toString->s2 to be fully evaluated before Time1.add(advance).toString->s1. Therefore the second time printed could be the initial time. The first time printed will always be the time after the advance(n) message is evaluated however, since the order in which messages are dealt with is fixed if one follows another in a sequence separated by the . symbol.

prev up next