On 10 Feb., 15:56, thomas.mer...@[EMAIL PROTECTED]
wrote:
Hello,
I have improved my chapter about object orientation.
Many thanks to all who gave suggestions.
It would be nice to get some feedback about the
new version before the final release. Here is it:
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 harddisk files, console output and
printer output. The implementation does totally different
things for this files. UNIX has used the "everything is a
file" philosopy 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. 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.
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 'circle's 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 philosopy a message is sent to an object.
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: All parameters get a user
defined name. In the above example the name 'aCircle' was used
for the 'self'/'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: result is circle.value;
begin
result.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, refered 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 type of the value of an 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: result is line.value;
begin
result.xLen := xLen;
result.yLen := yLen;
end func;
In addition we define a normal (not DYNAMIC) function
which draws 'shape's 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 PRIMITIVE_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 the function '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 redeclare 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 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 inferface 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: result is Float.value;
begin
result.val := flt(a.val) + b.val;
end func;
const func Float: (in Float: a) + (in Integer: b) is func
result
var Float: result is Float.value;
begin
result.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: result is Integer.value;
begin
result.val := a.val + b.val;
end func;
const func Float: (in Float: a) + (in Float: b) is func
result
var Float: result is Float.value;
begin
result.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 destinct types for different purposes.
================================
Thanks in advance for your effort.
Greetings Thomas Mertes
Seed7 Homepage: http://seed7.sourceforge.net
Seed7 - The extensible programming language: User defined statements
and operators, abstract data types, templates without special
syntax, OO with interfaces and multiple dispatch.


|