7. OBJECT ORIENTATION
Many people will be familiar with object-orientation from
languages like C++, Smalltalk, and Java. Seed7 follows the
route of declaring "interfaces". An interface is a common
set of operations supported by an object. For instance
cars, motorcycles, lorries and vans can all accelerate or
brake, if they are legal to drive on the road they can all
indicate right and left.
This view isn't new. C provides a primitive form of
interfacing. When you write to a 'file' in C you use the same
interface ('fprintf') for hard disk files, console output and
printer output. The implementation does totally different
things for these files. Unix has used the "everything is a
file" philosophy for ages (even network communication uses
the 'file' interface (see sockets)).
For short: An interface defines which methods are supported
while the implementation describes how this is done.
Several types with different method implementations can
share the same interface.
7.1 Interface and implementation
Seed7 uses interface types and implementation types.
Objects declared with an interface type refer to a value
which has an implementation type. This situation is
described with the following picture:
+----------------+
declared | interface |<--- interface type
object: | object | (known at compile-time)
+----------------+
|
| refer to value
V
+----------------+
value: | implementation |<--- implementation type
| object | (unknown at compile-time)
+----------------+
The interface type of an object can always be determined
at compile-time. Several implementation types can belong
to one interface type (they implement the interface type).
E.g.: The types null_file, external_file and socket
implement the file interface. On the other hand: An
implementation type can also implement several interface
types. An interface object can only refer to a value with
an implementation type that implements the interface.
E.g.: A shape variable cannot refer to a socket.
A new interface type is declared with:
const type: shape is new interface;
Interface (DYNAMIC) functions describe what can be done
with objects of an interface type. An interface function
for a shape could be:
const proc: draw (in shape param, inout window param) is DYNAMIC;
Now we know that it is possible to 'draw' a shape to a
window. How this drawing is done is described in the
implementation type. An implementation type for shape is:
const type: circle is new struct
var integer: radius is 0;
end struct;
The fact that the type circle is an implementation type of
shape is described with:
type_implements_interface(circle, shape);
The function which implements 'draw' for circles is:
const proc: draw (in circle: aCircle, inout window: aWindow) is func
begin
circle(aWindow.win, aWindow.currX, aWindow.currY,
aCircle.radius, aWindow.foreground);
end func;
In the classic OOP philosophy a message is sent to an object.
To express this situation classic OO languages use the
following method call syntax:
param1.method(param2, param3)
In the method the receiving object is referred with 'self' or
'this'. The other parameters use the same mechanisms as in
procedural programming languages (value or reference parameter).
Seed7 uses a different approach: Instead of an implicit defined
'self' or 'this' parameter, all formal parameters get a user
defined name. To reflect this symmetric approach a Seed7 method
call looks like a normal function call:
method(param1, param2, param3)
The definition of the 'draw' function above uses the formal
parameter 'aCircle' in the role of a 'self' or 'this' parameter.
Formal parameters which have an implementation type are
automatically in the role of a 'self' or 'this' parameter.
A function to create new circle objects can also be helpful:
const func circle: circle (in integer: radius) is func
result
var circle: aCircle is circle.value;
begin
aCircle.radius := radius;
end func;
Now we can draw a circle object with:
draw(circle(50), aWindow);
Although the statement above does exactly what it should
do and the separation between interface and implementation
is obvious, most OO enthusiasts would not be thrilled. All
decisions which implementation function should be called
can be made at compile time. To please the OO fans such
decisions must be made at runtime. This decision process
is called dynamic dispatch.
7.2 Dynamic dispatch
When the implementation types have different implementations
of the same function (method) a dynamic dispatch is necessary.
The type of the value, referred by an interface object, is not
known at compile-time. In this case the program must decide at
runtime which implementation of the function should be invoked.
This decision is based on the implementation type of the value
(referred by the interface object). A dynamic dispatch only
takes place when a DYNAMIC (or interface) function is called.
When the program is analyzed (in the interpreter or compiler)
the interface functions take precedence over normal functions
when both are to be considered.
To demonstrate the dynamic dispatch we define the type line
which also implements a shape:
const type: line is new struct
var integer: xLen is 0.0;
var integer: yLen is 0.0;
end func;
type_implements_interface(line, shape);
const proc: draw (in line: aLine, in window: aWindow) is func
begin
line(aWindow.win, aWindow.currX, aWindow.currY,
aLine.xLen, aLine.yLen, aWindow.foreground);
end func;
const func line: line (in integer: xLen, in integer: yLen) is func
result
var line: aLine is line.value;
begin
aLine.xLen := xLen;
aLine.yLen := yLen;
end func;
In addition we define a normal (not DYNAMIC) function
which draws shapes to the 'currWindow':
const proc: draw (in shape: aShape) is func
begin
draw(aShape, currWindow);
end func;
In the example above the call of the (DYNAMIC) interface
function is 'draw(aShape, currWindow)'. The
interface function declared with
const proc: draw (in shape param, inout window param) is DYNAMIC;
decides which implementation function has to be called.
The dynamic dispatch works as follows:
- For all parameters which have an interface type the
parameter is replaced with its value. In this case the
parameter 'aShape' is replaced by a value of type
circle or line.
- The same logic as in the analyze part of the compiler
is used to find the matching function. In this search
normal functions take precedence over interface functions.
- When a matching function is found it is called.
This process describes the principal logic of the dynamic
dispatch. In practice it is not necessary to execute the
analyze part of the compiler during the runtime. It is
possible to simplify this process with tables and function
pointers.
7.3 Inheritance
When a new struct type is defined it is possible to
inherit from an existing struct type. E.g.:
const type: external_file is sub null_file struct
var clib_file: ext_file is PRIMITIVE_NULL_FILE;
var string: name is "";
end struct;
That way the type external_file inherits the fields and
methods of null_file, which is declared as:
const type: null_file is new struct
var char: bufferChar is '\n';
var boolean: io_empty is FALSE;
var boolean: io_ok is TRUE;
end struct;
In most situations it makes sense when the implementation
types inherit from a basic implementation type such as
null_file. That way it is possible to define functions
which are inherited by all derived implementation types.
In the standard library getln is such a function:
const func string: getln (inout null_file: aFile) is func
result
var string: stri is "";
local
var string: buffer is "";
begin
buffer := gets(aFile, 1);
while buffer <> "\n" and buffer <> "" do
stri &:= buffer;
buffer := gets(aFile, 1);
end while;
aFile.bufferChar := buffer[1];
end func;
All inherited types of null_file inherit the function
getln, but they are also free to redefine it. In the
getln function above the function call 'gets(aFile, 1)'
uses the (DYNAMIC) interface function:
const func string: gets (inout file param, in integer param) is DYNAMIC;
In other OO languages the distinction between interface type
and basic implementation type is not done. Such languages
either use a dynamic dispatch for every method call (as Java
does) or need a keyword to request a dynamic dispatch (as C++
does with the 'virtual' keyword).
When assignments take place between inherited implementation
types it is important to note that structure assignments are
done with (deep) copies. Naturally such assignments can only
copy the elements that are present in both structures.
In the following example just the null_file elements
are copied from 'anExternalFile' to 'aNullFile':
const proc: example is func
local
var null_file: aNullFile is null_file.value;
var external_file: anExternalFile is external_file.value;
begin
aNullFile := anExternalFile;
write(aNullFile, "hello");
end func;
Although the variable 'anExternalFile' is assigned to
'aNullFile', the statement 'write(aNullFile, "hello")'
calls the write function (method) of the type null_file.
A new interface type can also inherit from an existing
interface type:
const type: shape is sub object interface;
Although inheritance is a very powerful feature it should
be used with care. In many situations it makes more sense
that a new type has an element of another type (so called
has-a relation) instead of inheriting from that type (so
called is-a relation).
7.4 Class methods
Many object-oriented programming languages support methods
that are associated with a class instead of an instantiated
object. Such methods are called class methods or static
methods. Seed7 supports class methods via attribute ('attr')
parameters which allow that a function is attached to a type:
const func circle: create (attr circle, in integer: radius) is
return circle(radius);
This 'create' function is attached to the type circle and can be called with
create(circle, 10)
Many languages require that the class name must precede the
method name when a class method is called (E.g. 'circle::create(10)'
in C++). In contrast to that 'attr' parameters are not
restricted to a specific parameter position. They can be used
in any parameter position as in the following example:
const func circle: create (in integer: radius, attr circle) is
return circle(radius);
This function can be called with
create(10, circle)
Attribute parameters can be used for any type not just for
interface and implementation types. Objects which do not
have a function type such as a character constant can also
be attached to a type:
const char: (attr char) . value is ' ';
This way attributes can be used to specify properties
of a type such as its default 'value'.
Programming languages such as Seed7 which support function
definitions outside a class can also use normal functions
instead of class methods. It is a matter of tast if a
function should be grouped to a type or if it should
exist stand alone and is called with:
circle(10)
7.5 Multiple dispatch
The Seed7 object system allows multiple dispatch (not to
be confused with multiple inheritance). The methods are
not assigned to one type (class). The decision which
function (method) is called at runtime is done based upon
the types of several arguments. The classic object
orientation is a special case where a method is connected
to one class and the dispatch decision is done based on
the type of the 'self' or 'this' parameter.
The classic object orientation is a single dispatch system.
In the following example the type Number is introduced
which is capable to unify numerical types. The type
Number is an interface type which defines
the interface function for the '+' operation:
const type: Number is sub object interface;
const func Number: (in Number param) + (in Number param) is DYNAMIC;
The interface type Number can represent an 'Integer' or a 'Float':
const type: Integer is new struct
var integer: val is 0;
end struct;
type_implements_interface(Integer, Number);
const type: Float is new struct
var float: val is 0.0;
end struct;
type_implements_interface(Float, Number);
The declarations of the converting '+' operators are:
const func Float: (in Integer: a) + (in Float: b) is func
result
var Float: sum is Float.value;
begin
sum.val := flt(a.val) + b.val;
end func;
const func Float: (in Float: a) + (in Integer: b) is func
result
var Float: sum is Float.value;
begin
sum.val := a.val + flt(b.val);
end func;
The declarations of the normal '+' operators (which do not convert) are:
const func Integer: (in Integer: a) + (in Integer: b) is func
result
var Integer: sum is Integer.value;
begin
sum.val := a.val + b.val;
end func;
const func Float: (in Float: a) + (in Float: b) is func
result
var Float: sum is Float.value;
begin
sum.val := a.val + b.val;
end func;
The type Number can be extended to support other
operators and there can be also implementations using
complex, bigInteger, bigRational, etc. . That way
Number can be used as universal type for math
calculation. Further extending can lead to an universal
type. Such an universal type is loved by proponents of
dynamic typed languages, but there are also good reasons
to have distinct types for different purposes.
7.6 Replacing pointers with interface types
Many languages have the concept of a pointer. It is possible
to implement data structures, such as lists and trees, with
pointers. Although Seed7 supports the concept of a pointer, they
are not well suited to describe such data structures. Instead of
pointers interface types can be used. This way list, trees and
other advanced data structures can be defined.
The following example shows how to do this:
The interface type element will be used as "pointer":
const type: element is new interface;
An implementation type for the empty element (emptyElement)
can be used as basic implementation type from which other
implementation types can inherit:
const type: emptyElement is new struct
end struct;
That the implementation type emptyElement implements the
interface type element is described with:
type_implements_interface(emptyElement, element);
Since every Seed7 expression has exactly one type, it is
necessary to define a special 'NIL' value (used with 'element.NIL')
for the type element:
const element: (attr element) . NIL is emptyElement.value;
Now the struct with two "pointers" and an integer can be
declared:
const type: treeElement is sub emptyElement struct
var element: left is element.NIL;
var element: right is element.NIL;
var integer: item is 0;
end struct;
Finally the type treeElement is defined as implementation
of the type element:
type_implements_interface(treeElement, element);
To allow the direct access to the structure elements 'left', 'right'
and 'item' for objects of type element the following declarations
are necessary:
const func element: (ref element param).left is DYNAMIC;
const varfunc element: (inout element param).left is DYNAMIC;
const func element: (ref element param).right is DYNAMIC;
const varfunc element: (inout element param).right is DYNAMIC;
const func integer: (ref element param).item is DYNAMIC;
const varfunc integer: (inout element param).item is DYNAMIC;
When all this was declared the following code is possible:
const proc: addItem (inout element: anElem, in integer: item) is func
begin
if anElem = element.NIL then
anElem := xalloc(treeElement.value);
anElem.item := item;
elsif item < anElem.item then
addItem(anElem.left, item);
elsif item > anElem.item then
addItem(anElem.right, item);
end if;
end func;
const proc: listItems (in element: anElem) is func
begin
if anElem <> element.NIL then
listItems(anElem.left);
write(" " <& anElem.item);
listItems(anElem.right);
end if;
end func;
const func integer: sum (in element: anElem) is func
result
var integer: sum is 0;
begin
if anElem <> element.NIL then
sum := anElem.item + sum(anElem.left) + sum(anElem.right);
end if;
end func;
New elements can be created with the function 'xalloc'.
This way interface and implementation types help to provide the
pointer functionality.
Pointers and interface types are not always the best solution.
Abstract data types like dynamic arrays, hash tables, struct
types and set types can also be used to declare data structures.
|