Object Orientated Progamming in ICI. Although ICI is object-based it is not a strictly object orientated language, it does not enforce the object oriented method upon the programmer unlike languages such as Java. ICI can, however, be used in a manner that is object oriented by using its inbuilt types to represent higher level objects and adopting a few simple conventions. When used in this manner ICI provides a very powerful and easy to use environment. The key facilities used to enable OOP in ICI is ICI's struct type. Recall that ICI's struct objects are really dictionaries, objects that contain some number of key/value pairs that map one object to any other object. The keys used with structs are typically strings and ICI provides syntactic sugar to allow the familiar dot-notation to operate on structs with the name following the dot used as a string key. Also recall that ICI struct objects have an idea of a super struct. When looking up a key in a struct the chain of super structs will be followed if the key is not found in a base struct. Given these abilities we can model an object system. Objects are implemented as ICI struct objects with each object being a separate struct, the "instance struct". When an object is created, via the function "new", it is assigned a "class". A class is a struct object that contains, amongst other things, a template used to provide initial values for the instance variables of the object, and the methods that operate on objects of this type. The class struct is made the super struct of each instance struct of that class. If a class inherits from another class then the class has the base class as its super and so on. This allows arbitrary levels of inheritence. Because the class is the super struct of the instance struct looking up names in the instance will search the super struct. This allows the super to provide the initial values of instance variables and to store function objects that are used as methods. The above description is a little terse, real code is far easy to understand! Here's an example class "definition", 1 extern Complex = [struct 2 3 new = [func (self, re, im) 4 { 5 auto re = 0.0, im = 0.0; 6 7 self.re = re; 8 self.im = im; 9 }], 10 11 toString = [func (self) 12 { 13 return sprintf("(%s,%s)", string(self.re), string(self.im)); 14 }], 15 ]; On line one we name the class. Classes are defined as struct objects and are typically stored in separate files and used as modules within ICI programs. By declaring the class object in the extern scope level we export it to its user. If we were just going to use the class within the one module we could define it in the static scope. Here we're defining a complex number class called "Complex". Line three introduces the "new" method for this object. A method is just a function that can be applied to an object (a struct). The new method is a little special, as we shall see below, in that it is called by the new function to initialise the newly created object. Apart from that new is just like any other method function. All methods are written to take, as their first parameter, the object used to invoke the method (this is important as we'll see below). I always name this argument "self" but it could be any valid ICI identifier. In the example our new method takes two optional parameters, re and im, that are the real and imaginary parts of the complex number. We store the actual values into the object. ICI's ability to provide default parameter values is used to make this new method able to be passed zero, one or two parameters with the default values being zero. We don't return a value from a class new method as the new function always returns the struct of the newly created object. A new method can however raise an error using ICI's fail function. Line eleven defines another method, toString, that returns a representation of the complex number as a string. We use sprintf to format the string. Note how we use string to turn the numbers into strings and output those. We don't know if the types of re and im are integer or real so we use string to do the conversion for us. We could also force the values to be floating point and always use %g format in sprintf. Using Objects With classes defined as above we can write the new function that creates new instances of a class. extern new(class) { auto vargs = [array]; self = struct(@class); call(class.new, array(self) + vargs); return self; } The new function creates a struct to store the instance and makes the class struct the super of the instance struct. Then the class new method is invoked on the new instance, passing any extra parameters given to new. Then we just return the object. The instance has a read-only version of the class as its super to stop it modifying the shared class. This is important as it allows the class to provide the initial values of instance variables. If a search for name in the instance struct fails the class is searched. If the name is assigned to then the new variable is created in the instance struct. With the above representation we can now do many OO things. We can define classes, use inheritence, create new objects, call methods, etc... All very OO. Calling Methods All the above is nice but there is a problem with the way methods are invoked. When we want to invoke a method on an object we have to explicitly pass the object as the first parameter to the method's function. This is in addition to using the object to determine the method. object.method(object, arguments...); This means we evaluate the object twice. If the object is an expression with side-effects we're in trouble. The binary-@ operator comes to the rescue. The binary-@ operator was introduced to provide a mechanism for calling methods on objects defined in the above way. A new syntax is introduced, object@method(arguments...) Which is functionally identical to, object.method(object, arguments...) This invokes the method on passing as an implicit first parameter to the method function (other arguments are passed following that). The binary-@ doesn't do the actual function call, the '()' operator still does that. Binary-@ forms an ICI pointer object from its operands and the semantics of the call operator are been modified to recognise this and perform a method call operation when applied to a pointer object. If you examine a method you'll see there are two parts to the method's name, there is the object and the method. A method name maps to a function that is object specific. When invoking the method we use the method name as a key and lookup its value within the object to find a function object that performs the method. This is similar to what happens when a pointer object in ICI is dereferenced. A pointer holds two values, an object and a key. When dereferenced we do a lookup in the object using the key. To support method calling the call operator, '()', is modified to allow calling pointers. Originally only function objects could be called. When call is applied to a pointer the object and key values in the pointer are used. The pointer is dereferenced to obtain the actual function object to be called (dynamic method lookup ICI style) and the object is passed as the first parameter to the function. Exactly what we want to call methods. To use our class we can now do, /* * Create a new instance of a complex number, setting the real * part to 1.3 and using the default for the imaginary (0). */ auto c = new(Complex, 1.3); /* * Output the complex number. */ printf("%s\n", c@toString()); /* * Access member variables. */ xyz = c.re * e; Object Ettiquette Inheritence Inheritence is performed by defining classes as super struct's of other classes. A class inherits from a base class by using it as its super. Again an example makes it clear. Say we have a graphics library with a general Shape class and more specific shape classes for things like rectangles and circles. The more specific shapes should inherit behaviour from the general shape class. Our general shape class has a few methods. extern Shape = [struct new = [func (self, type) { self.type = type; self.x = 0; self.y = 0; }], setPosition = [func (self, x, y) { self@undraw(); self.x = x; self.y = y; self@draw(); }], ]; More specific shapes inherit from Shape and extend its behaviour, extern Circle = [struct : Shape, new = [func (self, radius) { ... invoke super class new self.radius = radius; }], draw = [func (self) { ... do stuff to draw the circle }], undraw = [func (self) { }], ]; Notice how the general shape method setPosition invokes shape specific draw and undraw methods. Because the methods are dynamically bound the generic code can call a method defined in the specific shape instance. Calling the Super Class new method. Often a class that inherits from another class needs to invoke its super's new method. There is no problem with doing this however the syntax used to achieve it is a little confusing. Recall that the instance is a struct whose super is the class object. If the class inherits from another then the class object's super is set to the class object for the class it inherits from. We can use the super function to obtain the class objects. The expression super(self) returns the class for a particular instance and super(super(self)) returns the super class. So to call the super class new we have to use, super(super(self)).new(self); A not particular nice expression. It would be better if we could do something like, self@super(); To invoke the super class new method for an instance. To do so requires we have a definition of this super method somewhere in the inheritence chain for the object. This can be done by using a common base class for all objects with the convention that all classes eventually inherit from the common base class. We call the common base class Object. An Object Class The Object class is the common base class for all objects. The Object class defines a number of methods that are common to all objects. In particular it defines the super method used to call a super class new method for a newly created object. extern Object = [struct /* * Call the super class new function for this instance. * * To avoid infinite loops (always a good idea) we * temporarially replace the super of the object with * its super. This means each call to a super class new * method makes the object appear to be an instance of * that class as the instance struct's super is set to * the class meta-object for that class. We set the * super back before returning. */ super = [func (self) { auto vargs; auto s; if (s = super(self)) { super(self, super(s)); if (typeof(self.new) == "func") { try call(self@new, array(self) + vargs); onerror { super(self, s); fail(error); } } super(self, s); } }], ]; The super method is quite robust. It traps errors in the new method and maintains object integrity by ensuring the super is the same upon function exit. Note how the call function is used to invoke a method. In this case we are calling the method directly and must pass the implicit first parameter. Armed with our super method we can call our super class new method in the new method of our class. We can extend classes. extern MyText = [struct : Text, defaultFilename = "/usr/local/lib/app/app.1.gz", new = [func (self, parent) { self@super(parent); self@loadFile(self.defaultFilename); }], loadFile = [func (self, name) { auto f; if (name ~ #\.Z$# || name ~ #\.gz$#) f = popen("gzip -dc " + name); else f = name; super(super(self)).loadFile(self, f); if (typeof(f) == "file") close(f); }], ]; The loadFile method shows how to override a super class method. You simple define a method (function) with the same name in the child class and it overrides the super class object with that name. If the object is a method (function) it can call the super class definition of the method by looking it up in the super class and calling that function passing the instance struct as first parameter. There should be a better way to call overriden super class methods but the above technique will have to do for now.