Seed7 
 FAQ 
 Manual 
 Screenshots 
 Examples 
 Algorithms 
 Download 
 Links 

 Manual 
 Introduction 
 Tutorial 
 Syntax 
 Statements 
 Types 
 Parameters 
 Objects 
 File System 
 Declarations 
 Tokens 
 Expressions 
 OS access 
 Actions 
 Errors 

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.