[Seed7-users] Preview chapter about object orientation
Interpreter and compiler for the Seed7 programming language.
Brought to you by:
thomas_mertes
From: Thomas M. <tho...@gm...> - 2008-02-10 15:03:56
|
Hello, I am writing a chapter about object orientation. It would be nice to get some feedback about it before the final release. Here is it: 7. OBJECT ORIENTATION ===================== The object orientation of Seed7 is based on terms like interface and implementation. An interface defines which methods are supported while the implementation describes how this is done. Several classes with different method implementations share the same interface. This view is not something new. When you write to a file (in C under UNIX) you use the same interface (higher level printf or lower level write) for harddisk files, console output and printer output. The implementation does totally different things for this files. UNIX uses the "everything is a file" philosopy for ages (even network communication uses the file interface (see sockets)). 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 there is a 'draw' function declared for 'circle' it is not called directly. Instead the 'draw' interface function for 'shape' is called. When the program is analyzed (in the interpreter or compiler) interface functions take precedence over normal functions when both are to be considered. The interface function for 'shape' decides which implementation function has to be called. This decision process is called dynamic dispatch. In the above example the overhead of the dynamic dispatch seems to be unnecessary, but in general it is the way object orientation works also in other languages. 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 is described with a dynamic (or interface) function. 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 above example the call of the (DYNAMIC) interface function is 'draw(aShape, currWindow)'. For this call 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. This time 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'. In most situations it makes sense when the implementation types inherit from a basic implementation type such as 'NULL_FILE'. 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). 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. -- Psssst! Schon vom neuen GMX MultiMessenger gehört? Der kann`s mit allen: http://www.gmx.net/de/go/multimessenger |