Inheritance and Polymorphism in Perl

By: Damian Conway

Philologist recapitulates phylogeny1

 

1. Applied Laziness

Laziness is the essence of good design.

When you're building an extension to your house, or customizing a car, or upgrading your computer, you normally start with an existing blueprint and add on (or replace) certain bits. If your original blueprints are good, it's a waste of time and resources to start from nothing and separately reconstruct the bits you already have.

The same thing happens in object-oriented programming. Sometimes you already have a class of objects that partially meets your requirements (say a class that represents a truck), and you just want to tweak that class, to create one that exactly meets your needs (say a class that represents a fire-truck).

In an object-oriented language, it's not necessary to code that new class from scratch, reproducing (or maybe cutting and pasting) the original truck code, and then adding new methods to implement alarms, ladders, hoses, red braces, etc.

Instead, you can just tell the program that the new FireTruck class is based on (or inherits) the existing Truck class. Then you add certain extra features (i.e. additional data and methods) to the FireTruck class, over and above those it inherited from the Truck class. That's the Laziest possible solution, since you only have to specify the new stuff you want and then say "and do the rest the same way that class over there does it".

Any class like FireTruck that inherits attributes and methods from another class is called a derived class or sometimes a child class. The class from which it inherits (i.e. Truck in this case) is called its base class or its parent class. The relationship between a base class and its derived class is called the is-a relationship, because an object of a derived class must necessarily have all the attributes and methods of an object of the base class, so it "is a" base-class object for all practical purposes. This idea corresponds well with many relationships in the Real World: a fire-truck is-a truck, an automated teller machine is-a machine, a hench-person is-a person, an unnecessarily long list of analogies is-a list of analogies.

The is-a relationship is itself inherited (or perhaps, "meta-inherited"), so you can have increasingly general categories over more than two levels: a fire-truck is-a truck is-a vehicle is-a device is-a thing; a hench-person is-a person is-a animal is-a life-form is-a thing. If you trace most classes back to their most ancestral categories, they all end up being a "thing".

Note however that is-a isn't a two-way street. Though an object of a derived class can always be treated like an object of a base class, it's not true that an object of a base class necessarily is-a object of a derived class. That is, although a fire-truck always is-a truck, it's not the case that a truck always is-a fire-truck.
 

2. Generalized Laziness

Naturally, having created a useful base class like Truck, you're probably not going to stop at deriving a FireTruck class; you'll probably also want classes representing dump trucks, tow trucks, pickup trucks, armoured cars, cement mixers, delivery vans, monster trucks, and recreation vehicles. Each of these will (separately) inherit the same set of characteristics from the original Truck class, but each will extend or modify those characteristics uniquely. The relationship between the Truck class and some of its possible child classes is shown in Figure 1.
 

Figure 1 -- Inheriting from the Truck class



Using inheritance means that you only have to specify how a fire-truck or a cement mixer or an armoured car differs from a regular truck, rather than constantly needing to restate all the standard ways in which it's the same as a truck. That makes the code that defines each type of truck shorter, which means there's far less work to create it.

More importantly, it reduces your maintenance load too, because any change to the behaviour of the general Truck class (for example, modifying its register() method in response to some change in transport regulations) is automatically propagated to all the specific truck classes (FireTruck, DumpTruck, ArmouredCar, etc.) that inherit from Truck.

In this way, inheritance also provides a means of capturing the abstract relationships between specific classes of object within a program. The class of fire-trucks is a special case of the more general class of trucks, which in turn might be a special case of the very general class of vehicles. The more abstract classes are generalized blueprints that define the common features of a wide range of kinds of objects (such as the standard regulations covering all vehicles). The more specialized classes presuppose those common features, and then describe the additional attributes and methods unique to their particular kind of object (such as the special regulations covering fire-trucks).
 

3. Inheritance in Perl

Perl's approach to implementing all these useful features of inheritance is typically low-key and uncomplicated. A package that is acting as a class simply announces its "allegiance" to some other class, and thereby inherits all its methods. Perl also provides some standard methods that all classes inherit, and a small dose of syntactic sugar to make rewriting inherited methods easier. Let's start with the pledge of allegiance...
 

3.1.  It pays to choose your parents wisely

A class informs Perl that it wishes to inherit from another class by adding the name of that other class to its @ISA package variable. For example, the class PerlGuru could specify that it wishes to inherit from class PerlHacker as follows:
    package PerlGuru;
    @ISA = ( "PerlHacker" );
And that's it! From that point on, whenever Perl needs to determine if PerlGuru has any inherited methods, it checks the contents of the array @PerlGuru::ISA. Any package whose name appears in that array is considered to be a parent class of PerlGuru. Of course, since it's an array, we can have many class names in @PerlGuru::ISA, allowing the class to inherit methods from more than one parent (a phenomenon known somewhat unimaginatively as multiple inheritance):
    package PerlGuru;
    @ISA = qw( PerlHacker LanguageMaestro Educator PunMeister );
And, of course, if those four parent classes also inherited from other classes:
    package PerlHacker;
    @ISA = qw( Programmer Obfuscator );
    package PunMeister;
    @ISA = qw( Writer Humorist OneSickPuppy );
then PerlGuru would also inherit methods from those "grandparents". All this inheritance creates the hierarchy shown in Figure 2.
 

Figure 2 -- PerlGuru's inheritance hierarchy


 

3.2.  It don't mean a thing if it ain't got that hierarchical pre-order method selection semantics

Inheritance in Perl is a much more casual affair than in other object-oriented languages. In essence, inheritance means nothing more than: if you can't find the requested method in an object's blessed class, look for it in the classes that the blessed class inherits from.

In other words, if you call:

    my $guru = PerlGuru->new();
    # and later...
    my $question = <>;
    print $guru->answer($question);
then, if class PerlGuru doesn't provide a PerlGuru::answer method, Perl starts trying the various parent classes (as specified by the current value of the @PerlGuru::ISA array).

The parents are searched in a depth-first recursive sequence, so Perl would look for one of the following (in this order):

If any of these methods is defined, the search for a suitably endowed ancestor terminates at once and that method is immediately called. This process of searching for the right method to call is known as method dispatch.

Note that, when looking in a parent class, Perl checks the left-most parent first, and then checks the left-most parent of that class, and the left-most parent of that class etc. Hence, if a class's left-most great-great-great-grandparent has an answer method then that method will be called, even if another of the object's direct parents also has a suitable method.

If you're used to the complicated inheritance semantics in some other object-oriented language, it's important to realize that inheritance in Perl is merely a way of specifying where else to look for a method, and nothing else! There is no direct inheritance of attributes (unless you arrange for it), nor any hierarchical calling of constructors or destructors (unless you explicitly write those methods that way), nor any compile-time consistency checks of the interface or implementation of derived classes.

Inheritance just tells you were to look next for a missing method.
 

4. What's a method like you doing in a class like this?

If you've ever gone up to someone in a bar or club and introduced yourself, you know that people can respond to the very same message ("I'd like to get to know you better") in very different ways. If you were to categorize those ways, you might identify several classes of person: ReceptivePerson, IndifferentPerson, ShyPerson, RejectingPerson, RejectingWithExtremePrejudicePerson, JustPlainWeirdPerson.

But that's actually back-to-front, because the way in which a particular person will respond to your message depends on the kind of person they already are. A ReceptivePerson will respond enthusiastically, a ShyPerson will respond tentatively, and a JustPlainWeirdPerson will probably respond in iambic pentameter. The original message is always the same; the response depends on the kind of person who receives it.

Language theorists2 call this type of behaviour polymorphism. When a method is called on a particular object, the actual method that's involved depends on the class to which the object belongs. For instance, if you call an object's ignite() method, its response will be quite different depending on whether it belongs to the Paper, Rocket, Passion, or FlameWar class.

Randomly calling an identically named method on objects of different classes is not, of course, a recommended programming technique. However, polymorphic behaviour does prove extremely useful when there is some explicit relationship between the various classes of objects, or when there is an implicit relationship or a common universal property shared between them. The following subsections discuss each of these cases.
 

4.1.  Insanity is hereditary...

Suppose you're creating an object-oriented system for tracking the registration and inspection of trucks. You'd almost certainly want to use the Truck class (and its many descendants) to implement the parts of the system that represent individual trucks.

Typically, the objects representing the various trucks would be collected in some kind of container, probably a list. Some operations will need to be carried out on individual objects (e.g. register this particular truck, schedule an inspection for that one, etc.), but many tasks will have to be performed on every truck in the system (e.g. send out an annual registration notice for each, print a complete list of recent inspection dates, etc.)

For operations that need to be performed on every truck, the application is likely to walk along the list using a loop, calling the appropriate method for each object in turn. For example, the loop might call each object's print_registration_reminder() method.

The problem is that the actual procedure to be followed by each object may be different, depending on the actual kind of truck the object represents (i.e. the actual class to which it belongs). For instance, every truck needs to be registered, but the form for registering a semi-trailer may be very different from the one for a fire-truck, or for an armoured car. If that's the case, the processing loop will have to determine the class of each object and then find the corresponding method call for each distinct class. That's a pain to code, and a bigger pain to re-code every time you add or remove another class of truck.

This situation is the ideal place to use polymorphism. If the ancestral Truck class has a register() method, then you're guaranteed that every derived class also has a register() method (namely, the one that it inherits from Truck). However, when you specify the various derived classes, you may choose to replace the inherited register() method with one specific to the needs of the derived class.

Having given each class its own unique register() method, you can then walk the list of objects and simply call register() on each. You can be confident that each object can respond to that method call because at the very least they'll use the register() they inherited from the Truck class. However, if they have a more specialized way of registering themselves, then that more specialized method will be automatically invoked instead. In other words, you can arrange that each object has a register() method, but not necessarily the same register() method.

The result is that, although the loop code is very simple:

    for each object in the list...
        call its register() method
the response to those calls is always appropriate to the particular object on which the method is called.

Better yet, if you subsequently add a new class derived from Truck, and then put objects of that new class in the list, the old code will continue to work without modification. When the loop encounters an object of the new class, it will simply call that object's new register() method, and execute the new behaviour specified by the object's class definition. If the new class didn't define any new behaviour, the old behaviour inherited from class Truck will be used instead.

This kind of polymorphism is known as inheritance polymorphism, because the objects whose methods are called belong to a hierarchy of classes that are related by inheritance. The presence of the required method in the base class of the hierarchy ensures that objects of any derived class can always respond to a given method call. The ability to redefine individual methods in derived classes allows objects of those classes to respond more specifically to a particular method call, if they so wish.

All object-oriented languages support inheritance polymorphism (it's part of the definition of "object oriented"). For some languages, it's the only form of polymorphism they permit. But it certainly isn't the only form that's possible. In fact, there's no need for objects that are treated polymorphically to have any kind of class relationship at all.
 

4.2.  We don't need no stinking inheritance!

The alternative approach to polymorphism is to allow any object with a suitable method to respond to a call to that method. This is known as interface polymorphism, because the only requirement is that a particular object's interface provide a method of the appropriate name.

For example, since there are probably no actual Truck objects used in the truck registry application, there's no real need for the Truck class at all (at least, as far as the polymorphism in the registration loop is concerned). So long as each object in the list belongs to a class that has a register() method, the loop doesn't really care what their ancestral class was (i.e. whether they are trucks, truckers, trucking companies, or truculents). Provided they can each respond to a call on their register() method, the loop proceeds with serene indifference.

Of course, that's a mighty big proviso. With inheritance polymorphism you could be sure that every object in the list did have a register() method (at the very least, the one it inherited from Truck). With interface polymorphism there's no such guarantee.

Inheritance polymorphism is a special case of interface polymorphism, because a common base class guarantees that objects share a specific inherited method. Hence any language that supports interface polymorphism automatically supports inheritance polymorphism as well.

Perl is such a language.
 

5. Polymorphism in Perl

Whilst some object-oriented languages have special syntaxes and a long list of rules, constraints, and conditions on the use of polymorphic methods, as you'll have realized by now, Perl has a different attitude.

In Perl, every method of every class is (potentially) polymorphic, as a direct consequence of the way that methods are automatically dispatched up the class hierarchy (as described above). There's no special syntax, no requirement for type-compatibility of method arguments, no need for inheritance relationships between classes. Just define your method, redefine it in any derived classes that need to act differently, and without even knowing it you're polymorphizing.

Suppose we have an object reference (say, $datum) and we call a method (say, print_me) on it:

    foreach my $datum ( @data )
    {
        $datum->print_me();
    }
The method dispatch mechanism determines the class of the invoking object (i.e. of $datum), and then looks in the corresponding package for a method of the appropriate name (i.e. print_me). Provided the object belongs to a class with a method named print_me, the method call succeeds and some action is taken. That action depends on the class of the invoking object, even though the call syntax is always the same.

The elements in the @data array might have been blessed into completely unrelated classes:

    my @data = (
        GIF_Image->new(file=>"camelopard.gif", format=>"interlaced"),
        XML::File->new("./lamasery.xml"),
        PGP_Coded->new("Software is *not* a munition!"),
        HTTP::Get->new("http://www.perl.org/news.html"),
        Signature->new(),
    );


but the same method call (i.e. $datum->print_me()) handles them all appropriately, so long as each object's class's interface provides a print_me method. So Perl provides full interface polymorphism.

Of course, the dispatch mechanism also has a fall-back strategy if the class of the invoking object doesn't provide a matching method. As explained earlier, it immediately searches through the object's ancestor classes, trying to find an inherited method with the correct name.

This means that if the object belongs to a class that inherits a method named print_me, the method call succeeds and some ancestrally-defined action is taken. Once again, that action depends on the class of the particular object (or more accurately, on the "genealogy" of its class), even though the call syntax is still always the same. So Perl also provides full inheritance polymorphism.

As is usually the case in Perl, the choice is yours.
 

[This article is adapted from Damian Conway's new book "Object Oriented Perl"]
 


Footnotes

1 a.k.a. Damian Conway explains inheritance and polymorphism and how they work in Perl

2  ...most of whom live at ground-zero in the JustPlainWeirdPerson category...