Introduction to Classes and Objects
The purpose of the tutorial is to introduce you to the notions of object orientation as met in Visual Prolog, and to give you a quick exemplified introduction to the syntax.
The main semantic entities in the Visual Prolog object model are objects, object types and classes. The main syntactic notions to deal with these entities are interfaces, class declarations and implementations.
An interface is a named set of predicate declarations. Interfaces describe the "interface" of objects (hence the name), i.e. the access that you have to an object from outside the object. Interfaces represent object types.
Consider this interface definition:
interface person predicates getName : () -> string Name. setName : (string Name). end interface person
This is the definition of an interface named person. All objects of the person type have two predicates getName and setName, with declarations as stated above.
Interfaces only define types of objects; it is classes that create the objects. A class has a declaration and an implementation. A class that constructs person objects, might be declared like this:
class person : person constructors new : (string Name). end class person
This is the declaration of a class named person, which constructs objects of the person type. The class has a constructor named new, which given a Name will construct an object (i.e. of the person type).
The class will also need an implementation, which might look like this:
implement person facts name : string. clauses new(Name) :- name := Name. clauses getName() = name. clauses setName(Name) :- name := Name. end implement person
This is the implementation of the class named person. The implementation must provide a definition for each of the public predicates and constructors, i.e. for new, getName and setName. The implementation can also locally declare and define extra entities, which are only accessible in the implementation itself. In this case, the class declares a fact variable named name, which is used to store the name of the person.
Each object has its own instance of the fact variable name. And the code of the clauses above will all reference that particular instance of the fact variable. We say that the predicates are object predicates and that the fact is an object fact.
A class can also have entities that are shared among all objects of the class. Let us for example extend the example above with code that counts the number of person objects that are created. This count will increase every time an object is created and never decrease.
I will extend the declaration of the class with a predicate (i.e. getCreatedCount) that can return the current count:
class person : person constructors new : (string Name). predicates getCreatedCount : () -> unsigned Count. end class person
Notice that publicly accessible class predicates are declared in the class declaration, whereas publicly accessible object predicates are declared in interfaces. This is a rule without exceptions: it is not possible to declare object predicates in a class declaration, and it is not possible to declare class predicates in an interface.
The predicate will need a definition in the implementation of the class; I will also need a fact for storing the count. This fact must be a class fact (i.e. shared among all the objects). In the implementation of a class you can declare and define private object entities as well as private class entities. To declare class entities, you prefix the corresponding declaration section with the keyword class. All in all our class person can be implemented like this:
implement person class facts createdCount : unsigned := 0. clauses getCreatedCount() = createdCount. facts name : string. clauses new(Name) :- name := Name, createdCount := createdCount+1. clauses getName() = name. clauses setName(Name) :- name := Name. end implement person
I.e. I have added a class fact createdCount, which is initialized to zero. I have also added a clause for the predicate getCreatedCount, which returns the current value of createdCount. Finally, I have added the code in the constructor, which increments the createdCount.
Notice that in the constructor, two assignments have the same shape, but one updates the object state, whereas the other updates the class state.
A special variant of classes does not produce objects at all, and therefore they act as "modules" rather than classes. A non-object constructing class (or simply a module) is declared by omitting the object type in the declaration:
class io % no type here predicates write : (string ToWrite). write : (unsigned ToWrite). end class io
Such a class that does not produce objects (obviously) cannot contain object entities, neither can it have constructors.
Creation and Access
Given the code above I can create a goal that creates an object and uses the io-class (whose implementation I shall not consider here) to write the name of the person.
goal P = person::new("John"), Name = P:getName(), io::write(Name).
In the first line I call the constructor new in the class person. The created object is bound to the variable P. In the next line I bind the variable Name to the result of invoking the object predicate getName on P. In the last line I call the class predicate write in the class io.
Notice that names in classes are referenced using two colons, e.g. person::new. Also notice that object predicates are referenced using one colon, e.g. P:getName.
Finally, you should notice that constructors are functions that return an object, even though they are not declared like functions: The return type is subsumed from the class declaration.
Interfaces are Object Types
As mentioned above interfaces are object types. This should be taken literally, you can use interfaces in the same places where you can use non-object types. For example, in predicate declarations:
class mail predicates sendMessage : (person Recipient, string Message). end class mail
The predicate mail::sendMessage takes a person and a string as arguments.
You can create several completely different classes that all create person objects. You simply declare and implement more classes that construct person objects. The implementation of the classes can be very different, for example I can create a class that stores the person in a database. This is the declaration of such a class:
class personInDB : person constructors new : (string DatabaseName, string Name). end class personInDB
This tutorial is not concerned with actual implementations of this and that. But I hope the code below shows that objects of a certain object type, can have completely different implementation.
implement personInDB facts db : myDatabase. personID : unsigned. clauses new(DatabaseName, Name) :- db := myDatabase::getDB(DatabaseName), personID := db:storePerson(Name). clauses getName() = db:getPersonName(personID). clauses setName(Name) :- db:setPersonName(personID, Name). end implement personInDB
You will notice that not only the internal behavior is completely different, but the internal state also has a completely different structure and contents.
Objects of the same type can be used in the same context, no matter how different their implementations are. I can, for example, send a message to a person using the mail class declared above, no matter if that person is constructed by person or personInDB:
goal P1 = person::new("John"), mail::sendMessage(P1, "Hi John, ..."), P2 = personInDB::new("Paul"), mail::sendMessage(P2, "Hi Paul, ...").
This behavior is known as subsumption: Objects constructed by one class, are as good as objects constructed by another class in a certain context, as long as both objects have the type required by that context.
You will notice that the predicate mail::sendMessage can equally well take objects from any person class, so this predicate is polymorphic in a certain sense (Subsumption Polymorphism).
Supports (Type Extension)
Let us imagine that my program also deals with a special kind of persons, namely the users of the program. Users are persons with a name, but there is more to them, they also have a password. I want to create a new object type/interface for users, which states that a user is a person, with a password. For this I use the supports qualification:
interface user supports person predicates trySetPassword : (string Old, string New, string Confirm) determ. validatePassword : (string Password) determ. end interface user
It is stated that user supports person. This has two effects:
- It means that user objects will have to provide the predicates declared in the person interface (i.e. getName and setName).
- It also means that an object of type user is also an object of type person, and can therefore also be used in contexts that expect a person object.I.e. if we assume that I have a user class:
class user : user constructors new : (string Name, string Password). end class user
Then objects of that class can be used by mail::sendMessage:
goal P = user::new("Benny", "MyCatBobby"), mail::sendMessage(P, "Hi Benny, ...").
An interface can support several other interfaces, meaning that:
- the objects of that type must provide all the predicates in the supported interfaces
- the objects of that type have all the other types as well. An interface can also support one or more interfaces, which themselves support one or more interfaces, and so forth. And also in that case:
- the objects of that type must provide all the predicates in the indirectly as well as directly supported interfaces
- the objects of that type have all the other indirect as well as direct types. The supports qualifications generate a subtype hierarchy: we say that user is a subtype of person.
Object: the Ultimate Object Super-Type
An interface that does not explicitly support any interfaces, implicitly supports the interface object. object is an implicitly defined interface that has no contents (i.e. no predicates). Any object supports the object interface directly or indirectly, so any object has type object. And therefore object is the super type of all object types.
When I want to implement the class user, I will, of course, like to take an advantage of one of our person classes. Let us assume that the user class should resemble the class person, except of course that it also deals with the password. I would like our class user to inherit the implementation of the person part from class person. This is achieved by the following code:
implement user inherits person facts password : string. clauses new(Name, Password) :- person::new(Name), password := Password. clauses trySetPassword(Old, New, Confirm) :- validatePassword(Old), New = Confirm, password := New. clauses validatePassword(Password) :- password = Password. end implement user
This implementation states that it inherits person, which has the following effects:
- A person object is embedded into each constructed user object.
- All predicates from the person interface can be inherited directly from class person to class user.When you inherit a predicate you do not directly state the implementation of it, instead the implementation from the class that you inherit from is used.
The inheritance can to a certain extend be explained as syntactic sugaring. At least I could have achieved exactly the same effect with the following code (the clauses for the password predicates remains as above):
implement user facts person : person. password : string. clauses new(Name, Password) :- person := person_class::new(Name), password := Password. clauses getName() = person:getName(). clauses setName(Name) :- person:setName(Name). ... end implement user_class
In this code I do not inherit from class person, instead I create a person object and store it in a fact variable. And instead of inheriting the code for getName and setName I have made trivial implementations of these predicates, which simply delegate the task to the object in the fact variable. A section with the keyword delegate can be used as shortcut for such delegation
This code has very much the same effect, but there are some notable differences:
- First of all I have written more code.
- The person object is not embedded in the user object, instead there is a reference to it. (Also here there are two memory allocations instead of one).
- In the last situation I can dynamically change the value in the fact variable to another object; simply by assigning a new object to the fact variable. For example, to an object of class personInDB.You should also notice that the second implementation have an extra level of indirection in the call. Visual Prolog handles such indirections rather efficiently, but it handles the inheritance even more efficiently.
Visual Prolog has multiple inheritance, meaning that you can inherit from many classes simultaneously.
Above I have introduced you to the most fundamental concepts of the object system in Visual Prolog. There are other interesting features in the object system, but these will not be covered here, for example:
- Objects can support further interfaces in the implementation.
- Finalizers that are executed on storage reclaim.
- Object predicate values, a seamless counterpart to C# delegates.