The 2003 Perl Advent Calendar
[about] | [archives] | [contact] | [home]

On the 12th day of Advent my True Language brought to me..
Class::ISA

Sometimes, even the simplest of modules prove the most handy.

Have you ever been in the situation where you're digging through someone else's code and they call a method that you've never heard of before? It's not in the code you're using - it's inherited somehow - but what class does the method come from? And how do we work this out?

Inheritance is complicated in Perl - unlike many other languages it allows you to have multiple inheritance, meaning that you can get methods from one or more classes, and deciding which method is called in preference to which method.

All we really need is way of getting what a module inherits from, what those modules inherit from, and what modules those modules inherit from, and so on, in a big list so we can tell what module's method gets called first. And this is what Class::ISA does

The usual way that you declare a base class in Perl is to use the use base directive.

  package Flintstone::Fred;
  use base qw(Flintstone Person);

This declares that Flintstone::Fred is a class that inherits from two base classes ('superclasses') Person and Flintstone - unlike languages like Java in Perl you can inherit from as many classes as you want. Perl looks though each of it's superclasses in order to find subroutines that implement methods and it uses the first it finds. This means that if you call a method on Flinstone::Fred it'll look in the subroutine in Flintsone::Fred, then in Flintstone and then in Person.

The situation gets more complicated if Flintstone is itself a subclass of another superclass (or several superclasses) itself. Perl uses a depth first search algorithm which means that it'll look in all of Flintstone's superclasses for subroutines (and their superclasses and so on) before looking in Person's

Like most people, at this point my head is beginning to spin. What we need is a diagram. Luckily, the Graphviz extension Graphviz::ISA::Multi is able to create nice graphs for us.

  #!/usr/bin/perl
  # turn on perl's safety features
  use strict;
  use warnings;
  # get the name of the class
  my $class = shift
    or die "usage: $0 <classname>\n";
  # create the graph
  use GraphViz::ISA::Multi;
  my $graph = GraphViz::ISA::Multi->new();
  $graph->add($class);
  # open the file
  my $filename = $class;
  $filename =~ s/::/-/g;
  use IO::File;
  my $fh = IO::File->new("> $filename.png")
    or die "Can't open '$filename.png': $!";
  # write it out as a png
  print $fh $graph->as_png;

So for our module, we get this nice graph like so:

Using Class::ISA

Class::ISA is ridicously easy to use. Although it doesn't export subroutines, all you have to do to work out a module's superclass is directly is call the Class::ISA::super_path subroutine explicitly with the name of the module.

   #!/usr/bin/perl
   # turn on the safety features
   use strict;
   use warnings;
   # load the modules
   use Class::ISA;
   use Flintstone::Fred; 
   # print out the module search order
   foreach my $class (Class::ISA::super_path('Flintstone::Fred'))
     { print "$class\n" }

This prints out

  Flintstone
  Cartoon
  Entity
  Person

Note that Entity is only listed once in the list, the first time it's inherited from by Cartoon. Obviously if the methods weren't in it the first time, they won't be in it the second! There's no need for it to be kept in the method search list.

So let's move onto the problem of finding out methods. What we have to do is search though each of the classes looking for the method subroutine. We need to use (Class::ISA::self_and_super_path because we should obviously start looking in the bottom class itself for the method before checking it's superclasses. We also need to take care to add the UNIVERSAL class onto the list, as all classes 'inherit' from this class if they specify it or not if the class search list is exhausted.

  #!/usr/bin/perl
  # turn on the safety features
  use strict;
  use warnings;
  # load the support modules
  use Class::ISA;  
  # check the arguments
  unless (@ARGV == 2)
    { die "usage: $0 <modulename> <methodname>\n"; }
  # work out what the module we're loading and load it
  my $module = shift;
  eval "use $module";
  if ($@) { die "Can't load module '$module': $@" }
  # get the methodname
  my $methodname = shift;
  # look through each of the routines for our method
  my @classes = (Class::ISA::self_and_super_path($module),
	         "UNIVERSAL");
  
  # first look for the method
  foreach my $class (@classes)
  {
    if (defined(&{"${class}::${methodname}"}))
    {
      print "${module}->${methodname} = ${class}::${methodname}\n";
      exit 0;
    }
  }
  # failing that look for an AUTOLOAD
  foreach my $class (@classes)
  {
    if (defined(&{"${class}::AUTOLOAD"}))
    {
      print "${module}->${methodname} = ${class}::AUTOLOAD\n";
      exit 0;
    }
  }
  # give up
  print "${module}->${methodname} not found\n";
  exit 1;

And it works. We have a nice simple tool we can use from the command line now:

  bash-2.05b$ ./find_method Flintstone::Fred name
  Flintstone::Fred->name = Flintstone::name
  bash-2.05b$ ./find_method Flintstone::Fred wibble
  Flintstone::Fred->wibble = Person::AUTOLOAD

  • base
  • GraphViz::ISA::Multi
  • GraphViz