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
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
It may look as though this technique is directly adding clauses to the
<-/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
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
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
0.0E+00, its initial slot values. Then, it changes the values of the
y slots to 3.14159 and 2.71828, respectively. The
PointObj is bound to the point object.
It is possible, and sometimes quite useful, to create get and put
methods for slots that don't exist. For example, it is possible to
add a polar coordinate interface to the point class by defining
get and put methods for
theta, even though there are no
theta slots. The get methods might be defined as
Self >> r(R) :- Self >> x(X), Self >> y(Y), R is sqrt(X*X + Y*Y). Self >> theta(T) :- Self >> x(X), Self >> y(Y), T is atan(Y/X).
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
slots. It might be reasonable to add a get method for
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 ‘/’
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
y slots to 3.14159 and 2.71828, respectively.
| ?- create(point, PointObj), do_something(PointObj), PointObj >> x(3.14159), PointObj >> y(2.71828).
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
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
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
p_term slot. This is
similar to what happens when such a term is asserted to the Prolog
| ?- retractall(foo(_,_)), assert(foo(X,Y)), X = a, Y = b, foo(c,d).
However, this goal would fail, because
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
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).
reduce/4 predicate would be defined to divide
DO by their greatest common divisor, producing
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]).
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 don't 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
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,
would create an instance of Class and bind the variable
Object to that
instance. All slots would receive their (possibly default) initial
More generally, if the definition for Class contains a create method
Self <- create(Arguments) :- Body.
| ?- 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
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
date_stamp object is created (unless some other method for
this class invokes
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
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
destroy/1 does not require that you define a
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.
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.
:- 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.
define_method/3 predicate installs a method for an object of
the class, and the
undefine_method/3 predicate removes that
Suppose that the
date_stamp class, defined earlier, declared an
instance method to print the year of a
:- 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
| ?- create(date_stamp, DateObj), define_method(DateObj, print_year(Stream), print_roman_year(Stream, DateObj)).
date_stamp object is created in 1994, a
print_year message sent to it would print the current year as
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
undefine_method/3 specify a message operator. Instance methods can
only be defined for send messages.