10.13.2.3 Methods

Some methods are defined by method clauses, between the class/1 directive and the end of the class's definition. Others are generated automatically. There are three kinds of messages in SICStus Objects, distinguished by the message operator they occur with:

`>>'
A get message, which is typically used to fetch values from an object's slots.
`<<'
A put message, which is typically used to store values in an object's slots.
`<-'
A send message, which is used for other operations on or involving an object.

SICStus Objects automatically generates some get and put methods. And, it expects particular message names with the send operator for create and destroy methods. For the most part, however, you are free to use any message operators and any message names that seem appropriate.

A method clause has one of these message operators as the principal functor of its head. Its first argument, written to the left of the message operator, is a variable. By convention, we use the variable Self. Its second argument, written to the right of the message operator, is a term whose functor is the name of the message and whose arguments are its arguments.

For example, in the class whose definition begins as follows, a 0-argument send message named increment is defined. No parentheses are needed in the clause head, because the precedence of the `<-' message operator is lower than that of the `:-' operator.

     :- class counter = [public count:integer = 0].
     
     Self <- increment :-
             Self >> count (X0),
             X1 is X0 + 1,
             Self << count (X1).

Its definition uses the automatically generated get and put methods for the public slot count.

It may look as though this technique is directly adding clauses to the >>/2, <</2 and <-/2 predicates, but the method clauses are transformed by term expansion, at compile time. However, the method clauses have the effect of extending the definitions of those predicates.

Methods are defined by Prolog clauses, so it is possible for them to fail, like Prolog predicates, and it is possible for them to be nondeterminate, producing multiple answers, upon backtracking. The rest of this section describes different kinds of methods.

Get and Put Methods

Get and put methods are generated automatically for each of a class's public slots. These are 1-argument messages, named after the slots.

In the point class whose definition begins with

     :- class point =
             [public x:float=0,
              public y:float=0].

the get and put methods are automatically generated for the x and y slots. If the class defines a create/0 method, the command

     | ?- create(point, PointObj),
          PointObj >>  x(OldX),
          PointObj >>  y(OldY),
          PointObj <<  x(3.14159),
          PointObj <<  y(2.71828).

creates a point object and binds both OldX and OldY to 0.0E+00, its initial slot values. Then, it changes the values of the x and y slots to 3.14159 and 2.71828, respectively. The variable PointObj is bound to the point object.

It is possible, and sometimes quite useful, to create get and put methods for slots that do not exist. For example, it is possible to add a polar coordinate interface to the point class by defining get and put methods for r and theta, even though there are no r and theta slots. The get methods might be defined as follows:

     Self >> r(R) :-
             Self >> x(X),
             Self >> y(Y),
             R2 is X*X + Y*Y,
             sqrt(R2, R).
     
     Self >> theta(T) :-
             Self >> x(X),
             Self >> y(Y),
             A is Y/X,
             atan(A, T).

This assumes that library(math), which defines the sqrt/2 and atan/2 predicates, has been loaded. The put methods are left as an exercise.

In the rational number class whose definition begins with

     :- class rational =
             [public num:integer,
              public denom:integer].

get and put methods are automatically generated for the num and denom slots. It might be reasonable to add a get method for float, which would provide a floating point approximation to the rational in response to that get message. This is left as an exercise.

It is also possible to define get and put methods that take more than one argument. For example, it would be useful to have a put method for the point class that sets both slots of a point object. Such a method could be defined by

     Self << point(X,Y) :-
             Self << x(X),
             Self << y(Y).

Similarly, a 2-argument get method for the rational number class might be defined as

     Self >> (N/D) :-
             Self >> num(N),
             Self >> denom(D).

Note that the name of the put message is (/)/2, and that the parentheses are needed because of the relative precedences of the `>>' and `/' operators.

Put messages are used to store values in slots. Get messages, however, may be used either to fetch a value from a slot or to test whether a particular value is in a slot. For instance, the following command tests whether the do_something/2 predicate sets the point object's x and y slots to 3.14159 and 2.71828, respectively.

     | ?- create(point, PointObj),
          do_something(PointObj),
          PointObj >> x(3.14159),
          PointObj >> y(2.71828).

The fetch_slot/2 predicate can similarly be used to test the value of a slot.

The effects of a put message (indeed, of any message) are not undone upon backtracking. For example, the following command fails:

     | ?- create(point, PointObj),
          PointObj << x(3.14159),
          PointObj << y(2.71828),
          fail.

But, it leaves behind a point object with x and y slots containing the values 3.14159 and 2.71828, respectively. In this, storing a value in an object's slot resembles storing a term in the Prolog database with assert/1.

Some care is required when storing Prolog terms containing unbound variables in term slots. For example, given the class definition that begins with

     :- class prolog_term = [public p_term:term].
     
     Self <- create.

the following command would succeed:

     | ?- create(prolog_term, TermObj),
          TermObj << p_term(foo(X,Y)),
          X = a,
          Y = b,
          TermObj >> p_term(foo(c,d)).

The reason is that the free variables in foo(X,Y) are renamed when the term is stored in the prolog_term object's p_term slot. This is similar to what happens when such a term is asserted to the Prolog database:

     | ?- retractall(foo(_,_)),
          assert(foo(X,Y)),
          X = a,
          Y = b,
          foo(c,d).

However, this goal would fail, because c and d cannot be unified:

     | ?- create(prolog_term, TermObj),
          TermObj << p_term(foo(X,X)),
          TermObj >> p_term(foo(c,d)).

Direct Slot Access

Get and put methods are not automatically generated for private and protected slots. Those slots are accessed by the fetch_slot/2 and store_slot/2 predicates, which may only appear in the body of a method clause and which always operate on the object to which the message is sent. It is not possible to access the slots of another object with these predicates.

You may declare a slot to be private or protected in order to limit access to it. However, it is still possible, and frequently useful, to define get and put methods for such a slot.

For example, if numerator and denominator slots of the rational number class were private rather than public, it would be possible to define put methods to ensure that the denominator is never 0 and that the numerator and denominator are relatively prime. The get methods merely fetch slot values, but they need to be defined explicitly, since the slots are private. The new definition of the rational number class might start as follows:

     :- class rational =
             [num:integer=0,
              denom:integer=1].
     
     Self >> num(N) :-
             fetch_slot(num, N).
     
     Self >> denom(D) :-
             fetch_slot(denom, D).
     
     Self >> (N/D) :-
             Self >> num(N),
             Self >> denom(D).

One of the put methods for the class might be

     Self << num(NO) :-
             fetch_slot(denom, DO)
             reduce(NO, DO, N, D),
             store_slot(num, N),
             store_slot(denom, D).

where the reduce/4 predicate would be defined to divide NO and DO by their greatest common divisor, producing N and D, respectively.

The definition of reduce/4 and the remaining put methods is left as an exercise. The put methods should fail for any message that attempts to set the denominator to 0.

Send Methods

Messages that do something more than fetch or store slot values are usually defined as send messages. While the choice of message operators is (usually) up to the programmer, choosing them carefully enhances the readability of a program.

For example, print methods might be defined for the point and rational number classes, respectively, as

     Self <- print(Stream) :-
             Self >> x(X),
             Self >> y(Y),
             format(Stream, "(~w,~w)", [X, Y]).

and

     Self <- print(Stream) :-
             fetch_slot(num, N),
             fetch_slot(denom, D),
             format(Stream, "~w/~w", [N, D]).

These methods are used to access slot values. But, the fact that the values are printed to an output stream makes it more reasonable to define them as send messages than get messages.

Frequently send methods modify slot values. For example, the point class might have methods that flip points around the x and y axes, respectively:

     Self <- flip_x :-
             Self >> y(Y0),
             Y1 is -1 * Y0,
             Self << y(Y1).
     
     Self <- flip_y :-
             Self >> x(X0),
             X1 is -1 * X0,
             Self << x(X1).

And, the rational number class might have a method that swaps the numerator and denominator of a rational number object. It fails if the numerator is 0.

     Self <- invert :-
             fetch_slot(num, N)
             N =\= 0,
             fetch_slot(denom, D)
             store_slot(num, D),
             store_slot(denom, N).

These methods modify slot values, but they do not simply store values that are given in the message. Hence, it is more reasonable to use the send operator.

It is possible for a method to produce more than one answer. For example, the class whose definition begins with

     :- class interval =
             [public lower:integer,
              public upper:integer].

might define a send method

     Self <- in_interval(X) :-
             Self >> lower(L),
             Self >> upper(U),
             between(L, U, X).

which uses the between/3 predicate from library(between). The in_interval message will bind X to each integer, one at a time, between the lower and upper slots, inclusive. It fails if asked for too many answers.

The rest of this section describes particular kinds of send messages.

Create and Destroy Methods

Objects are created with the create/2 predicate. When you define a class, you must specify all the ways that instances of the class can be created. The simplest creation method is defined as

     Self <- create.

If this method were defined for Class, the command

     | ?- create(Class, Object).

would create an instance of Class and bind the variable Object to that instance. All slots would receive their (possibly default) initial values.

More generally, if the definition for Class contains a create method

     Self <- create(Arguments) :-
             Body.

the command

     | ?- create(Class(Arguments), Object).

will create an instance of Class and execute the Body of the create method, using the specified Arguments. The variable Object is bound to the new instance.

If a simple class definition has no create methods, it is impossible create instances of the class. While the absence of create methods may be a programmer error, that is not always the case. Abstract classes, which are classes that cannot have instances, are often quite useful in defining a class hierarchy.

Create methods can be used to initialize slots in situations when specifying initial slot values will not suffice. (Remember that initial values must be specified as constants at compile time). The simplest case uses the arguments of the create message as initial slot values. For example, the definition of the point class might contain the following create method.

     Self <- create(X,Y) :-
             Self << x(X),
             Self << y(Y).

If used as follows

     | ?- create(point(3.14159, 2.71828), PointObj),
          PointObj >> x(X),
          PointObj >> y(Y).

it would give X and Y the values of 3.14159 and 2.71828, respectively.

In some cases, the create method might compute the initial values. The following (partial) class definition uses the date/1 predicate from library(date) to initialize its year, month and day slots.

     :- class date_stamp =
             [year:integer,
              month:integer,
              day:integer].
     
     Self <- create :-
             date(date(Year, Month, Day)),
             store_slot(year, Year),
             store_slot(month, Month),
             store_slot(day, Day).

All three slots are private, so it will be necessary to define get methods in order to retrieve the time information. If no put methods are defined, however, the date cannot be modified after the date_stamp object is created (unless some other method for this class invokes store_slot/2 itself).

Create methods can do more than initialize slot values. Consider the named_point class, whose definition begins as follows:

     :- class named_point =
             [public name:atom,
              public x:float=1,
              public y:float=0].
     
     Self <- create(Name, X, Y) :-
             Self << name(Name),
             Self << x(X),
             Self << y(Y),
             assert(name_point(Name, Self)).

Not only does the create/3 message initialize the slots of a new named_point object, but it also adds a name_point/2 fact to the Prolog database, allowing each new object to be found by its name. (This create method does not require the named_point object to have a unique name. Defining a uniq_named_point class is left as an exercise.)

An object is destroyed with the destroy/1 command. Unlike create/2, destroy/1 does not require that you define a destroy method for a class. However, destroy/1 will send a destroy message (with no arguments) to an object before it is destroyed, if a destroy method is defined for the object's class.

If a named_point object is ever destroyed, the address of the object stored in this name point/2 fact is no longer valid. Hence, there should be a corresponding destroy method that retracts it.

     Self <- destroy :-
             Self >> name(Name),
             retract(name_point(Name, Self)).

Similar create and destroy methods can be defined for objects that allocate their own separate memory or that announce their existence to foreign code.

Instance Methods

Instance methods allow each object in a class to have its own method for handling a specified message. For example, in a push-button class it would be convenient for each instance (each push-button) to have its own method for responding to being pressed.

The declaration

     :- instance_method Name/Arity, ....

inside a class definition states that the message Name/Arity supports instance methods. If the class definition defines a method for this message, it will be treated as a default method for the message.

The define_method/3 predicate installs a method for an object of the class, and the undefine_method/3 predicate removes that method.

Suppose that the date_stamp class, defined earlier, declared an instance method to print the year of a date_stamp instance.

     :- instance_method print_year/1.
     
     Self <- print_year(Stream) :-
             Self >> year(Y0),
             Y1 is YO + 1970,
             format(Stream, "~d", [Y1]).

The arithmetic is necessary because UNIX dates are based on January 1, 1970.

If a particular date_stamp object's date were to be printed in Roman numerals, it could be given a different print_year method, using the define_method/3 predicate.

     | ?- create(date_stamp, DateObj),
          define_method(DateObj,
     		   print_year(Stream),
     		   print_roman_year(Stream, DateObj)).

If this date_stamp object is created in 1994, a print_year message sent to it would print the current year as

     MCMXCIV

Defining the predicate print_roman_year/2 is left as an exercise. It must be able to access the year slot of a date_stamp object. Because it is not defined by a method clause within the class definition, print_roman_year/2 cannot use the get_slot/2 predicate.

None of instance_method/1, define_method/3, undefine_method/3 specify a message operator. Instance methods can only be defined for send messages.


Send feedback on this subject.