Dynamic dispatch
While inheritance and polymorphism can be useful in and of themselves, they are relatively limited and incapable of dealing with truly complex relationships between base and extension classes. More specifically, they provide no way of having extension-class specific processing performed when needed.
For example, suppose there is an Animal class (used by a zoo) with many extensions (for each species of animal). There might be code somewhere to manage the feeding of the animals. But because of the different eating patterns of the various animals, there's probably no general (that is, non-family or non-species specific) way to determine the next feeding time for a particular animal. So how best to do this?
The answer lies in dynamic dispatch, as described in the following sub-sections.
Overridable versus Abstract methods
You can declare a function in the Animal
class as overridable:
class animal public ... function nextFeeding is float overridable ... end public ...
The actual implementation of this overridable method then
provides a default NextFeeding
method for all Animal
class objects.
For example, the following implementation makes the rather silly
assumption that most animals need to be fed every eight hours (with
seconds as the time units):
function nextFeeding is float overridable return %this:lastFeeding + 8 * 60 * 60 end function
Now, any class that extends the Animal
class can provide an alternative
NextFeeding
function that overrides, or implements, the base class function:
class bat extends animal public ... function nextFeeding is float - implements nextFeeding in animal ... end public function nextFeeding is float - implements nextFeeding in animal ... bat specific next feeding calculations end function ...
But how does one invoke the appropriate NextFeeding
method for a particular
object?
The answer is, by simply invoking the NextFeeding
method in the Animal
class:
%creature is object animal ... print %creature:nextFeeding
When the Nextfeeding
method is invoked in the above example, SOUL
determines the true class (remember, %creature
can reference something
of the Animal
class, or of any extension class of Animal
) of the object
referenced by %creature
, and it calls the appropriate NextFeeding
method
for that object.
Note that this actually achieves a narrowing assignment (assignment of
a base class variable to an extension class variable), because the
object reference from the base class variable is assigned to the %this
variable, which is of the extension class, in the extension class method.
This is a very structured and specialized narrowing assignment that
avoids many of the problems and pitfalls generally associated with
narrowing assignments.
For example, in the following code, the Bat
class NextFeeding
method
will be called, because %creature
will reference a Bat
object:
%creature is object animal ... %creature = %(bat):new print %creature:nextFeeding
Any extension class of the Animal
class that doesn't implement the
NextFeeding
method will simply have the Animal
class's NextFeeding
method run on its behalf.
Suppose there were no sensible default behavior for a method (as is the
case, really, for the Nextfeeding
function).
Suppose it was expected that extension classes would always provide
a class-specific implementation of such a method.
Then, instead of declaring the method Overridable, the base class
could declare the method as Abstract:
public ... function nextFeeding is float abstract ... end public
The term Abstract
here means that the containing class provides no
implementation of the indicated method, but instead, it expects an extension
class to do so.
Now, if a base class has at least one Abstract method declared,
what if the method is invoked on an object
of the base class only, that is, not for an extension class?
The answer is that there is no sensible thing to do, so you are not
allowed to create an instance of an object of the base class only.
Declaring an Abstract method for a class makes the class itself an abstract class. This must be indicated on at least the first declaration for the class, by specifying the Abstract keyword following the class name:
class animal abstract ... public ... function nextFeeding is float abstract ... end public ...
If a class is abstract, you can declare variables of that class:
%sickAnimal is object animal
But you cannot create an object instance of that class.
That is, if class Animal
is abstract, both of the following two New
invocations are invalid:
%sickAnimal is object animal ... %sickAnimal = new ... %sickAnimal = %(animal):new
However, if a class extends the base class and implements all
its abstract methods, an object instance of that class can be assigned
to an abstract class variable.
For example, if the Bat
class implemented all the Animal
class
abstract methods, the following would be perfectly valid:
%sickAnimal is object animal ... %sickAnimal = %(bat):new
Note that it is possible to declare a class as Abstract
even if it has
no Abstract methods.
You might do this to leave open the possibility of
adding abstract methods to the class in the future.
Or you might do this if, for
whatever reason, it does not make sense to have an instance
of a base class — the class only makes sense when extended.
An abstract class can be extended by another abstract class, and an abstract method can be implemented by another abstract method:
class nocturnalAnimal extends animal public ... function nextFeeding is float - implements nextFeeding in animal abstract ... end public ...
In the case of the preceding example, an extension class
of the nocturnalAnimal
extension presumably
implements the Nextfeeding
method.
It is even possible to have an abstract class extend a concrete (non-abstract) class, and to have an abstract method implement an overridable method.
If a concrete class extends an abstract class with abstract methods, all abstract methods in the base class must be implemented in the concrete extension class.
Sirius Mods 7.2 introduced Protected variable declarations. Protected variables are class variables that can be examined anywhere, but can only be updated by the containing class or by extension classes of that class. Protected variables can be thought of as providing an overridable variable facility and can be considered an alternative to ReadOnly Overridable properties. For more information about Protected variables see "Variable declarations".
Method definition rules
The following rules pertain to the definitions of overridable, abstract, or implementing methods:
- Overridable or abstract methods can be any of the SOUL method types: Functions, Subroutines, or Properties.
- Overridable or abstract methods can also have any type of parameter allowed in any other methods: Required, Optional, NameAllowed, NameRequired, AllowNull, etc.
- Methods that implement base class methods must have identical parameter signatures to the base class methods. That is, all parameter names, parameter datatypes, optional, required, NameAllowed, NameRequired, and other parameter attributes must be the same in the abstract or overridable methods and in the methods that implement them.
- Methods that implement base class properties and functions must have the same datatype for for their return or set values as the base class methods.
- Methods that implement base class methods do not have to have
the same method name as the base class method name.
This is valid:
function nightfeeding implements nextFeeding in animal
- Methods that implement abstract or overridable base class methods
can be in the Public or the Private block of the extension class:
- If in the Public block, the method can be invoked directly by users of the extension class without going through dynamic dispatch.
- If in the Private block, the code outside the class can only invoke the method via dynamic dispatch via the base class method.
- An overridable or abstract method may be inside the
private block of the base class:
class animal private ... function nextFeeding is float abstract ... end private ...
In such a case, the dynamic dispatch calls may only be made from inside the base class. But extension classes may implement the method, or if the method is abstract, as in this case, they must implement the method.
Super members
Sometimes, you may want a method that implements an overridable method to augment rather than replace the overridden method's processing, that is, to do some extra processing before or after the overridden method. An apparent way to accomplish this is to simply invoke the overridden method from inside the implementing method:
function nextFeeding is float implements nextFeeding in animal ... stuff before base method processing %foo = %this:(animal)nextFeeding ... stuff after base method processing end function
Unfortunately, the above syntax would simply result in the recursive calling
of the extension class's implementing method — %this
references an object of the extension class, so via dynamic dispatch,
the extension class's NextFeeding
method would be called.
To get around this, a special member name, Super, is provided.
The Super member exists only in methods that implement an overridable method, and it refers to the overridden method without dynamic dispatch. To call an overridden method from an implementing method, you use the Super member like this:
function nextFeeding is float implements nextFeeding in animal ... stuff before base method processing %foo = %this:super ... stuff after base method processing end function
As usual, the this:
above can be left off,
so %this:super
can be written simply as %super
.
If the method had parameters, they could be included after the Super
,
as if the Super
was the overridden method name.
The Super designation makes no sense for methods that implement abstract methods, since with abstract methods, there is no base class method for the Super member to reference.
SOUL versus other languages
For programmers familiar with other object-oriented programming languages, it is worth pointing out that most of the facilities provided for dynamic dispatch by SOUL are no different than those provided by those other languages. However, some of the defaults in some other languages are different. For example:
- In some languages, all methods are assumed to be overridable
unless some keyword (usually
Final
) is specified after their declarations. - In some languages, a method in an extension class with the same name as an overridable method in a base class is automatically considered to implement the base class method. In SOUL, the Implements clause is required in a method declaration for a method to be considered to implement a base class method.
These differences are present in SOUL to facilitate its support for multiple inheritance, and also, independent of multiple inheritance, to improve the maintainability of SOUL code.
A final point: in many programming languages, the use of dynamic dispatch has a severe performance penalty. This is not the case for SOUL. While there is an exceedingly small performance penalty for using dynamic dispatch, this penalty is so small as to be unmeasurable in most cases. Performance cost should not be a consideration when trying to determine whether dynamic dispatch should be used.