The ICI Programming Lanaguage: Contents Previous chapter Next chapter
In object-oriented ICI programs, "objects" are structs that have specific properties. This is a bit confusing because I have been using the term "object" to refer to any ICI primitive type. This is historical. To avoid further confusion I will use "class" and "instance" explicitly instead of "object" when talking about object-oriented techniques.
ICI supports object-oriented programming by building on the properties of structs to implement scoping in the same way that vanilla function calls do. The principal feature that supports object-oriented programming in ICI is calls to methods as opposed to calls to functions. Contrasting the two:
A method is a primitive ICI object that is a pairing of a subject object (the instance), and a function.
Consider the following simple fragment which creates a class:
extern an_extern = 1;
static a_static = 2; static a_class = [class a_func(arg) { this.value := arg + 1; return value + 2; } ];
After executing this code, a_class will refer to a new struct which is unremarkable except that its super has been automatically set to the static scope. Diagramatically:
We can create an instance of the class by invoking the new method on the class. For example:
an_inst = a_class:new();
The new method is a class method that exists in the global scope, so all classes effectively inherit it from there.
The new instance is, again, a struct that it unremarkable except that its super has been set to the class. In this simple example there are, as yet, no instance variables. So the instance is an empty struct. Diagramatically:
We are now in a position to invoke the a_func method on our new instance with, say:
x = an_inst:a_func(3);
The transfer of control into the function creates a struct for auto variables as usual, but rather than making the super of this struct the static scope the function was defined in, it is set to the instance that is the subject of this method call. Also, the local variables this and class are set automatically. Diagramatically, just after the first line of code in the function is executed:
After execution, x will be 6. Notice the use of the
:=
operator and the explicit use of this to force the creation of value in the instance. Otherwise it would have implicitly appeared as a local variable. This is, of course, only required when the instance variable doesn't already exist.
The instance is a normal struct. Thus we can reference the value instance variable with:
an_inst.value
Note that the instance has the class and outer scopes in its super chain. Thus we can also refer to:
an_inst.a_func an_inst.a_static an_inst.an_extern
Sub-classes are class structs that have another class as their super. The following example illustrates a number of aspects of sub-classing:
static sub_class = [class:a_class a_class_variable = 0, new(name) { o = this:^new(); o.name := name; o.a_count := 0; return o; } a_func() { this:^a_func(); ++a_count; } ];
After parsing we have a variable sub_class whoes super is a_class. Diagramatically:
To make a new instance of the sub-class we would execute:
subclass_inst = sub_class:new("a name");
The new function was defined in the sub-class, overiding the global new function. In this case new is a class function that expects to be called on the class itself, not an instance of the class. There is nothing that distinguishes class functions from ones that operate on an instance, except their operation and documentation.
To complete its operation, the new function coded here needs to call the new of the super-class. To do this it uses the
:^
operator which forms a method, but using the super of the current value of the class variable. There isn't actually a new coded in the super-class, but it will find the global new.
To work with sub-classes and overidden functions it is important to understand how the this and class variables are set in method calls.
Consider the call:
subclass_inst:a_func();
Before the first line of code is executed, the scope will look like this:
The class variable has been set by the method call mechanism to the class of the function being called. Functions being parsed within the scope of a class definition record their class, so it was not the super of the instance that set the class variable, but the class recorded by the function.
The first thing the sub-class a_func function does is call the same function in its super-class. Upon arrival in that function, the scope will look like:
In short, the class variable is always the class of the function, irrespective of any sub-classing the instance may be derived from (or any funny business done by changing the super of the instance).
Finally, note that class variables can simply be included in the class definition (as shown by a_class_variable in the example). They exist in the class and have no effect on any instance.
As has been seen, the static scope present when a class is defined forms the super for the class. In effect, the outer scopes can be considered outer classes. Functions defined in those scopes may, if appropriately coded, be class functions for these hypothetical top-level classes. For example, we could define a default debug method that we expect some classes to override:
extern dump() { forall (k, v in this) printf("%s=%a, ", string(k), v); printf("\n"); }
This function would be available to all instances of all classes. The class of such a function is the scope it was defined in.
All name binding is dynamic in ICI. This leads to a number of common constructs that are worthy of highlighting, because they are not seen in statically bound languages such as C++.
The commonest of these is polymorphic functions that work equally well with any object instance that falls within the scope of their definition, irrespective of its class. We saw a simple example of this above with the dump function. That function had no prerequisites on the object it was applied to. But in real applications it is more common to define functions that state they will do blah, providing the instance they are applied to has fields called whatever, that can be interpreted in such-and-such a way. For example:
/* * Return the distance across the diagonal of the * boundig box for any object that support a bounding * box recorded as xmin, xmax, ymin, ymax. */ extern bbox_diagonal() { dx = xmax - xmin; dy = ymax - ymin; return sqrt(dx * dx + dy * dy); } /* * Grow the bounding box of the object to ensure it * will account for a r radian rotation of any object * contained within the original bounding box. The * bounding box is assumed to be recorded in xmin, * xmax, ymin, ymax. */ extern bbox_grow_for_rotation(r) { ... }
ICI does not support multiple inheritance as such. But it is common and useful to use composite classes and/or global methods that provide the same effect.
The standard global methods available to all ICI instances or classes are summarised below. See the chapter on core language functions for detailed descriptions of each:
inst:copy()
inst:isa(class)
class:new()
inst:respondsto(name)
The ICI Programming Lanaguage: Contents Previous chapter Next chapter