2009-05-23

Moose's MOP: Browse

Use Case

I realized that it would be very instructive to accompany each of the articles in the Moose's Metaobject Protocol series with a solid use case. While metaclass programming isn't particularly difficult (remember, it's just OOP), it takes some experience to fully wrap your head around it. I also want to demonstrate that MOPs certainly have their uses, since advanced techniques might come off as solutions looking for problems.

Today I present a very simplistic class browser. It shows you a structured view of a class's superclasses, subclasses, and methods. You can easily see which methods come from which classes, and which methods override superclass methods. The browser is written in about a hundred lines of Perl (plus comments). It uses only the MOP concepts you've been introduced to, though admittedly some of the details are different. I would've written the previous article a little differently had I this idea sooner!

I encourage you to follow along with a copy of the code from GitHub. You can also look at a static copy for DateTime.

Real Working Code!

sub display_class {
    my $class = shift;

    Class::MOP::load_class($class);

    return html {
        body {
            h1 { $class }

            h3 { 'Superclasses' };
            display_superclasses($class);

            h3 { 'Subclasses' };
            display_subclasses($class);

            h3 { 'Methods' };
            display_methods($class);
        }
    }
}

This is the outermost function for class-browsing. It takes the name of a class (such as DateTime::Infinite) and returns an HTML document. This program uses the novel Template::Declare for the tedious work of generating correct HTML.

We start by loading the class. We're letting the user inspect any class, so we need to ensure that the class is loaded and ready for inspection. We then divide the work of displaying the class into three sections: superclasses, subclasses, and methods.

sub link_class {
    my $class = shift;
    a { href is "/$class"; $class }
}

link_class is just a little helper function to render a class name as a hyperlink. This adds some dynamicism to the application, letting you click through to see the hierarchy from other classes' perspectives.

sub display_superclasses {
    _display_hierarchy($_[0], 'superclasses');
}

sub display_subclasses {
    _display_hierarchy($_[0], 'direct_subclasses');
}

It turns out that displaying superclasses and subclasses is the same operation. The only difference is which method is called, so I factored out that logic into the _display_hierarchy helper function.

These two functions take a class name as a parameter. Each calls the helper function with that class name and an additional argument, a method name. The method will be called on the class's metaclass to generate the next layer of inheritance. This helper function is called recursively for each class in the hierarchy. The final result is a nicely formatted tree of classes. As an example, here's the end result for DateTime's subclasses:

sub _display_hierarchy {
    my ($class, $method) = @_;

    my $meta = Moose::Meta::Class->initialize($class);
    my @classes = $meta->$method
        or return '';

    ul {
        for my $other_class (@classes) {
            li {
                link_class($other_class);
                _display_hierarchy($other_class, $method);
            }
        }
    }
}

This function is the workhorse of displaying sub- and superclass trees. It collects the classes (based on the method name that's passed in) that represent the next layer of hierarchy, displays each one, then recurses.

You may not have seen $object->$method before. Most modern OOP languages permit this simple form of reflection. Instead of invoking the literal method name, you invoke a method name which is in a variable. Simple and handy, but it's a real pain in C++ and Java.

This reflection is perfect for us, since we display superclasses and subclasses the same way. For the list of direct superclasses, we call $meta->superclasses. For direct subclasses, we call $meta->direct_subclasses. Yes, this naming inconsistency does suck!

The rest of the function is pretty straightforward. Multiple inheritance would muck up the display, but oh well. That's more a fault of HTML being unequipped to sanely display graphs than it is a fault of the meta-model, which copes well enough with MI.

sub display_methods {
    my $class = shift;
    my $meta  = Moose::Meta::Class->initialize($class);

    ul {
        my %seen_method;

        for my $superclass ($meta->linearized_isa) {
            my $super_meta = Moose::Meta::Class->initialize($superclass);

            li {
                link_class($superclass);
                ul {
                    for my $method (sort $super_meta->get_method_list) {
                        if ($seen_method{$method}) {
                            li {
                                style is 'text-decoration: line-through';
                                $method
                            }
                        }
                        else {
                            li { $method }
                        }
                        $seen_method{$method} = 1;
                    }
                }
            }
        }
    }
}

This is a big one. There are three major things happening here. We're iterating over the linearized_isa of the class. This is the order of classes that method dispatch takes (with duplicates removed). It's the tree of all superclasses, but flattened into a list.

Secondly, we're iterating over each class's get_method_list. This is similar to the get_all_method_names that you've seen before. The difference is that get_method_list does not include inherited methods. Only methods defined in that particular class are included.

Thirdly, we're treating methods that we've seen already as special. When we see a method for the first time, we display it normally. When we see that method again, we display it with a strike-through. This indicates that the method was overridden. We know the method was actually overridden because we iterate over the classes in method-resolution order.

The net effect is that we show every level where methods were defined and overridden in the overall class hierarchy. This could be useful for learning an unfamiliar project's design, or in hunting down a strange regression (caused by an accidental override).

That is it for the class-browsing aspect of this. What remains is the business of hooking this class browser up to the web. For this I use the excellent HTTP::Engine module.

sub handle_request {
    my $request = shift;

    my ($class) = $request->request_uri =~ m{^/([\w:]+)$};
    $class ||= 'DateTime';

    my $body = display_class($class);
    HTTP::Engine::Response->new(body => $body);
}

This is a callback for HTTP::Engine. HTTP::Engine passes to the callback a request object, and we're expected to return a response object. We dispatch on the request's request_uri, which will just be the class name.

my $engine = HTTP::Engine->new(
    interface => {
        module          => 'ServerSimple',
        args            => { port => 1978 },
        request_handler => 'main::handle_request',
    },
);
$engine->run;

And finally we let HTTP::Engine do its thing.


Hopefully this has demonstrated a real (if simplistic) use of the information a metaclass contains. This could be extended into a real class browser which also includes documentation (extracted with your POD module of choice), each method's code (thanks to B::Deparse), a lint report of that code (using the wonderful Perl::Critic), and any other information you might have handy. None of these things actually relate to the metaobject protocol, so they weren't included in the code. However, the metaobject protocol could serve well as the basis of such a system.

1 comments:

brunov said...

Padre has an open ticket for a code browser http://padre.perlide.org/ticket/63.

Just saying ;)

Post a Comment