Seed7 - The extensible programming language
Seed7 FAQ Manual Screenshots Examples Libraries Algorithms Download Links
Manual Introduction Tutorial Declarations Statements Types Parameters Objects File System Syntax Tokens Expressions OS access Actions Foreign funcs Errors
Objects Interface Dynamic Inheritance Class methods Dispatch Pointers
Manual
Objects
 previous   up   next 

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 taste 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.


 previous   up   next