Previous chapter To contents Next chapter

Chapter 6, Object orientation

As mentioned several times, Pike is object oriented. This does not mean that it is identical to C++ in any way. Pike uses a less strict approach to object orientation which creates a more relaxed programming style. If you have never come in contact with object oriented programming before, be warned that the ideas expressed in Pike and in this chapter are my own and do not necessarily reflect what other people think about object oriented programming.

6.1 Terminology

As mentioned before, Pike uses a different terminology than C++ does. This has historic reasons, but might be changed in the future. In the meantime, you might benefit from this mini-dictionary which translates Pike-ish terms to C++-ish terms:
a class
a class
a clone
an instance of a class
to clone
to create an instance
an object
an instance of a class
a program
a class

6.2 The approach

Think of the data type program as an executable file. Then we clone this program and create an object. The object is then a running program. The object has its own data and its own functions, however, it can work together with other programs by calling functions in those objects. The functions can be thought of as message carriers, TCP sockets or just a way for programs to communicate. Now we have a running system with many running programs, each performing only the task it was designed for.

This analogy has one major flaw, when running programs in UNIX they actually run simultaneously. UNIX is multitasking, Pike is not. When one object is executing code, all the other objects has to wait until they are called. An exception is if you are using threads as will be discussed in a later chapter.

6.3 How does this help?

Ok, why is it a good idea to use object oriented programming? Well if you believe what you hear, the biggest advantage is that you can re-use your code in several projects. In my experience this is not the case.

In my experience, the advantages of object oriented programming are:

Modular programming made easy
Using The approach makes it easy to divide a project into smaller pieces, these pieces are usually easier to write than the whole.
Local data scope
This is a very nifty thing with object oriented programs. If your program uses several files, windows, stacks, TCP connections or whatever, you simply write a program that handles one such file, window, stack or TCP connection. If correctly written, you can then just create many clones of that program.
Using the same interface to different objects
I can write a function that take a stream as an argument and writes data to this stream. Later I might wish to write this data to a window instead. I can then create an object that has the same methods as a stream (specifically the write method) and send that to the function that outputs the data.
Most of these things can be done without object orientation, but it is object orientation that makes them easy.

6.4 Pike and object orientation

In most object oriented languages there is a way to write functions outside of all classes. Some readers might think this is what we have been doing until now. However, in Pike, all functions have to reside within a program. When you write a simple script in Pike, the file is first compiled into a program then cloned and then main() is called in that clone. All this is done by the master object, which is compiled and cloned before before all other objects. What the master object actually does is:
program scriptclass=compile_file(argv[0]); // Load script
object script=scriptclass(); // clone script
int ret=script->main(sizeof(argv), argv); // call main()
Similarly, if you want to load another file and call functions in it, you can do it with compile_file(), or you can use the cast operator and cast the filename to a string. You can also use the module system, which we will discuss further in the next chapter.

If you don't want to put each program in a separate file, you can use the class keyword to write all your classes in one file. We have already seen an example how this in chapter 4 "Data types", but let's go over it in more detail. The syntax looks like this:

class class_name {
    class_definition
}
This construction can be used almost anywhere within a normal program. It can be used outside all functions, but it can also be used as an expression in which case the defined class will be returned. In this case you may also leave out the class_name and leave the class unnamed. The class definition is simply the functions and programs you want to add to the class.

To make it easier to program, defining a class is also to define a constant with that name. Essentially, these two lines of code do the same thing:

class foo {};
constant foo = class {};
Because classes are defined as constants, it is possible to use a class defined inside classes you define later, like this:
class foo
{
    int test() { return 17; }
};

class bar
{
    program test2() { return foo; }
};

6.5 Inherit

A big part of writing object oriented code is the ability to add functionality to a program without changing (or even understanding) the original code. This is what inherit is all about. Let's say I want to change the hello_world program to write a version number before it writes hello world, using inherit I could do this like this:
inherit "hello_world";
int main(int argc, array(string) argv)
{
    write("Hello world version 1.0\n");
    return ::main(argc,argv);
}
What inherit does is that it copies all the variables and functions from the inherited program into the current one. You can then re-define any function or variable you want, and you can call the original one by using a :: in front of the function name. The argument to inherit can be one of the following:
A string
This will have the same effect as casting the string to a program and then doing inherit on this program.
A constant containing a program.
Any constant from this program, module or inherited program that contains a program can be inherited.
A class name
A class defined with the class keyword is in fact added as a constant, so the same rule as above applies.

Let's look at an example. We'll split up an earlier example into three parts and let each inherit the previous part. It would look something like this:


Note that the actual code is not copied, only the list of references. Also note that the list of inherits is copied when you inherit a program. This does not mean you can access those copied inherits with the :: operator, it is merely an implementation detail. Although this example does not show an example of a re-defined function, it should be easy to see how that works by just changing what an identifier is pointing at.

6.6 Multiple inherit

You can inherit any number of programs in one program, you can even inherit the same thing more than once. If you do this you will get a separate set of functions and variables for each inherit. To access a specific function you need to name your inherits. Here is an example of named inherits:
inherit Stdio.File; // This inherit is named File
inherit Stdio.FILE; // This inherit is named FILE
inherit "hello_word"; // This inherit is named hello_world
inherit Stdio.File : test1; // This inherit is named test1
inherit "hello_world" : test2; // This inherit is named test2

void test()
{
    File::read(); // Read data from the first inherit
    FILE::read(); // Read data from the second inherit
    hello_world::main(0,({})); // Call main in the third inherit
    test1::read(); // Read data from the fourth inherit
    test2::main(0,({})); // Call main in the fifth inherit
    ::read(); // Read data from all inherits
}
As you can see it would be impossible to separate the different read and main functions without using inherit names. If you tried calling just read without any :: or inherit name in front of it Pike will call the last read defined, in this case it will call read in the fourth inherit.

If you leave the inherit name blank and just call ::read Pike will call all inherited read() functions. If there is more than one inherited read function the results will be returned in an array.

Let's look at another example:

#!/usr/local/bin/pike
                
inherit Stdio.File : input;
inherit Stdio.File : output;
                
int main(int argc, array(string) argv)
{
    output::create("stdout");
    for(int e=1;e<sizeof(argv);e++)
    {
        input::open(argv[e],"r");
        while(output::write(input::read(4096)) == 4096);
    }
}
This short piece of code works a lot like the UNIX command cat. It reads all the files given on the command line and writes them to stdout. As an example, I have inherited Stdio.File twice to show you that both files are usable from my program.

6.7 Pike inherit compared to other languages

Many other languages assign special meaning to inherit. Most common is the notion that if you inherit a class, it means that your class should obey the same rules as the inherited class. In Pike, this is not necessarily so. You may wish to use inherit in Pike like this, but you can just as well choose not to. This may confuse some programmers with previous experience in object oriented programming.

6.8 Modifiers

Sometimes, you may wish to hide things from inheriting programs, or prevent functions from being called from other objects. To do so you use modifiers. A modifier is simply a word written before a variable definition, function definition, class definition or an inherit that specifies how this identifier should interact with other objects and programs. These modifiers are available:
static
Static hides this identifier from the index and arrow operators, which makes it impossible for other objects to call this function unless a function pointer to it is returned from inside this program.
final
This prevents other objects from re-defining this identifier in programs that inherit this program.
local
This makes the identifier 'local' meaning that even if it is overloaded in an inheriting program, this program will still use this identifer.
private
This prevents inheriting programs from accessing this identifier. Note that inheriting program can still re-define the identifier. Also note that private does not imply static.
public
This is the opposite of private. This is the default for all identifiers. public can be used to override the effects of a private inherit.
protected
Reserved for future use.
When modifiers are used in conjunction with inherit, all the variables, functions and classes copied from the inherited class will be modified with the keywords used. For instance, private inherit means that the identifiers from this inherit will not be available to program inheriting this program. static private inherit will also hide those identifiers from the index and arrow operators, making the inherit available only to the code in this program.

6.9 Operator overloading

Sometimes you want an object to act as if it was a string, an integer or some other data type. It is especially interesting to be able to use the normal operators on objects to allow short and readable syntax. In Pike, special methods are called when an operator is used with an object. To some extent, this is work in progress, but what has been done so far is very useful and will not be subject to change.

The following table assumes that a and b are objects and shows what will be evaluated if you use that particular operation on an object. Note that some of these operators, notably == and ! have default behavior which will be used if the corresponding method is not defined in the object. Other operators will simply fail if called with objects. Refer to chapter 5 "Operators" for information on which operators can operate on objects without operator overloading.

Operation Will call Or
a+b a->`+(b) b->``+(a)
a+b+c+d a->`+(b,c,d) (b->(a))+c+d
a-b a->`-(b) b->``-(a)
a&b a->`&(b) b->``&(a)
a|b a->`|(b) b->``|(a)
a^b a->`^(b) b->``^(a)
a>>b a->`>>(b) b->``>>(a)
a<<b a->`<<(b) b->``<<(a)
a*b a->`*(b) b->``*(a)
a*b*c*d a->`*(b,c,d) b->`*(a)*c*d
a/b a->`/(b) b->``/(a)
a%b a->`%(b) b->``%(a)
~a a->`~()  
a==b a->`==(b) b->`==(a)
a!=b !( a->`==(b) ) !( b->`==(a) )
a<b a->`<(b) b->`>(a)
a>b a->`>(b) b->`<(a)
a<=b !( b->`>(a) ) !( a->`<(b) )
a>=b !( b->`<(a) ) !( a->`>(b) )
(int)a a->cast("int")  
!a a->`!()  
if(a) { ... }  !( a->`!() )  
a[b] a->`[](b)  
a[b]=c a->`[]=(b,c)  
a->foo a->`->("foo")  
a->foo=b a->`->=("foo",b)  
sizeof(a) a->_sizeof()  
indices(a) a->_indices()  
values(a) a->_values()  
a(b) a->`()(b)  

Here is a really silly example of a program that will write 10 to stdout when executed.

#!/usr/local/bin/pike
class three {
    int `+(int foo) { return 3+foo; }
};

int main()
{
    write(sprintf("%d\n",three()+7));
}
It is important to know that some optimizations are still performed even when operator overloading is in effect. If you define a multiplication operator and multiply your object with one, you should not be surprised if the multiplication operator is never called. This might not always be what you expect, in which case you are better off not using operator overloading.

6.10 Simple exercises

  • Make a program that clones 10 hello world and then runs main() in each one of them.
  • Modify the register program to use an object for each record.
  • Modify the register program to use the following search function:
    void find_song(string title)
    {
        string name, song;
        int hits;

        title=lower_case(title);

        foreach(indices(records),name)
        {
            if(string song=records[name][title])
            {
                write(name+"; "+song+"\n");
                hits++;
            }
        }

        if(!hits) write("Not found.\n");
    }

  • Previous chapter To contents Next chapter