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:
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 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)).
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.
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.
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 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.