Perhaps my keenest frustration with Perl 5 is its lack of a clean metaprogramming facility. Stevan Little’s Class::MOP has filled in much of the gap. Here’s what I learned after playing with it for an afternoon.
Class::MOP Review
While writing and talking about Perl Hacks (also known as the Perlinomicon), several people asked me about which hacks just will not work in Perl 6. My estimate is around half, for various reasons.
For one, Perl 6 will make a lot of these hacks unnecessary. Improving object orientation, making subs and methods different, and adding real signatures cleans up a lot of issues in Perl 5.
For another, a lot of these hacks work with the underlying machinery of
perl in too much detail. Perhaps my biggest gripe with Perl 5 is
that it’s possible to do clever things too often only by knowing about its
internals. There’s really no clean, extensible, coherent, and orthogonal
metaprogramming facility. (I realize some people think that clean, coherent,
and orthogonal are bizarre words to use when talking about Perl, at least in
the positive sense, but there is a relationship between consistency and power
and usability which smart language designers can exploit.)
It’s not that hard things are impossible, merely that sometimes their implementations are difficult.
Metaprogramming
Consider, for example, the mechanism of super method dispatch in Perl:
sub some_method
{
my $self = shift;
log( 'some_method called' );
$self->SUPER::some_method( @_ );
}
The SUPER::some_method() call is relatively ugly, even from an
aesthetic perspective. perl has a pretty good idea of what method
you called, so it should be able to dispatch to the parent method easily.
There’s a bigger problem though. SUPER::some_method()
dispatches based on the compile-time package of the method. That is, whatever
package contained the method when Perl compiled the method will be the one that
determines where the dispatcher looks next. This absolutely prohibits
the use of SUPER::some_method() calls in roles or other forms of
object and class composition, as the compile-time package does not matter
nearly as much as the run-time object. This seems, to me, to be an accident of
the implementation now baked into the untasty and yet not-so-good-for-you pie
of backwards compatibility.
Simon Cozens wrote the SUPER module to
work around this. I maintain it now. With that module, the code can be:
sub some_method
{
my $self = shift;
log( 'some_method called' );
$self->SUPER( @_ );
}
… or even:
sub some_method
{
log( 'some_method called' );
super();
}
Thus it is possible to rewrite how Perl handles method dispatch. However, the internals of the module may frighten you. They bother me a little bit, not only because they work around a mud pie, but because they’re complex. There’s no easy or clean way from pure Perl to ask meta questions about an object or class, such as “Who are your ancestors?” and “Which ancestor contains this method?” and “What is your method dispatch order?”
A Meta-Object Protocol
Stevan Little’s Class::MOP attempts
to rectify this by providing a meta-object protocol for Perl 5. (There’s a
formal definition, but think of it as a standard set of semantics for how
classes and objects behave hidden behind a defined interface and you know as
much as you need to know to start.)
With a capable MOP, you can not only perform all of the necessary class and object operations cleanly (define a class, create an object, add a method, perform reflective queries on classes and objects, set an ancestor, remove a method), but you can override those operations for other uses. If you want or need to make classes behave differently in different circumstances, you can change, for example, the method resolution and dispatch order.
You don’t have to do that often to make such a thing useful — even if you only use the MOP’s reflective capabilities, having a clean interface to do so is nice.
To test Class::MOP, I decided to rewrite a few parts of
SUPER to use its reflection, not the reflection that Simon and I
have built. (I don’t mean to say that I helped Simon build the first versions
of SUPER, merely that I’ve added a couple of features and revised
the internals somewhat to do so.)
From One MOP to Another
I skimmed the available documentation (start with Class::MOP,
then move to Class::MOP::Class
for the technical details). The examples focus on using the MOP to build new
classes and objects directly. The docs say nothing about making the MOP
available to all Perl classes, regardless of their method of
declaration, but I thought they implied that the all-important
meta() method would be available.
This method returns the metaclass object for an invocant. This object is the
entry point into all of the reflective goodness described in
Class::MOP::Class. If I were to use the MOP reflection in
SUPER, I would need this to be available on arbitrary, ordinary
objects, as I can’t guarantee and don’t want to enforce that people always use
Class::MOP to create their objects and classes to use my little
module.
I asked Stevan Little about this issue and he quickly responded that creating a new metaclass instance for a given object instance or class required the code:
my $meta = Class::MOP::Class->initialize( $instance );
Even if $instance is a standard Perl object defined in the
classical (that is, non-MOP) way, now $meta works with it. That’s
what I wanted.
SUPER works by examining the call stack via
caller() to find the name of the invoking method. Then it finds
the parents of the current invocant and tries to find the next method to
invoke, while walking up the inheritance or method resolution chain in the
appropriate invocation order.
Doing that without the MOP requires looking in @ISA for each
parent, recursively, or calling a barely-documented
__get_parents() hook I had to add recently. Ick.
Doing that with the MOP required a quick rewrite of
get_parents() to call class_precedence_list() on the
metaclass object, to get a list of the ancestors of the invocant. The function
needs a little bit more logic though, as the call to SUPER() can
come from anywhere in the precedence chain, and the dispatch needs to go to the
next level up from the current level, not the first level above the
invocant.
Making that work is a simple partitioning algorithm I already had figured out and working. It took only a few minutes to replace code I disliked (but had debugged heavily) with method calls on the metaclass objects. The result is much cleaner, at least for my code.
All of my tests pass without modification. I have a lot of confidence in
Class::MOP.
Conclusion
I didn’t try Class::MOP for defining new classes and
creating new objects (it’s awfully verbose, and I don’t have a project where
that’s useful at the moment), but I really did want better reflection (at
least, behind a sane interface). It provides that.
I’m sure there’s a significant speed hit for using this approach in my code,
so I’m not sure if it will survive benchmarking… but it does impress me that
reflection in Perl 5 can be so much saner. (Besides, having a nice interface
and a good test suite makes it possible to rewrite Class::MOP and
even SUPER in XS for a huge speed boost.)
As usual, documentation lags behind the usefulness of this code. I do
recommend looking through Class::MOP::Class for a good idea of
what the modules can do; if you’ve used reflection productively in another
language, you may find this interface much cleaner than the package variable
and symbol table mucking that Perl 5 normally provides.
--- lib/SUPER.pm~ 2006-06-19 15:12:42.000000000 -0700
+++ lib/SUPER.pm 2006-06-19 15:12:25.000000000 -0700
@@ -29,6 +29,8 @@
package SUPER;
+use Class::MOP;
+
use strict;
use warnings;
@@ -41,47 +43,44 @@
use Carp;
use Scalar::Util 'blessed';
-use Sub::Identify ();
sub find_parent
{
- my ($class, $method, $prune, $invocant) = @_;
- my $blessed = blessed( $class );
- $invocant ||= $class;
- $class = $blessed if $blessed;
- $prune ||= '';
-
- my @parents = get_all_parents( $invocant, $class );
+ my ($class, $method, $prune) = @_;
+ my $blessed = blessed( $class );
+ $class = $blessed if $blessed;
+ $prune ||= '';
- for my $parent ( @parents )
+ for my $parent ( get_parents( $class, $prune ) )
{
- if ( my $subref = $parent->can($method) )
- {
- my $source = Sub::Identify::sub_fullname( $subref );
- next if $source eq "${prune}::$method";
- return ( $subref, $parent );
- }
+ my $parent_meta = Class::MOP::Class->initialize( $parent );
+ next unless $parent_meta->has_method( $method );
+ my $subref = $parent->can( $method );
+ return ( $subref, $parent );
}
}
-sub get_all_parents
+sub get_parents
{
- my ($invocant, $class) = @_;
+ my ($invocant, $prune) = @_;
+ my $meta = Class::MOP::Class->initialize( $invocant );
+ my @parents = $meta->class_precedence_list();
+
+ # remove the object's class; it's not useful
+ shift @parents;
+ return @parents unless $prune;
- my @parents;
+ my @previous;
- if ( $invocant->can( '__get_parents' ) )
- {
- @parents = $invocant->__get_parents();
- }
- else
+ # remove everything above the current level too
+ while ( my $parent = shift @parents )
{
- no strict 'refs';
- @parents = @{ $class . '::ISA' };
+ push @previous, $parent;
+ last if $parent eq $prune;
}
- return 'UNIVERSAL' unless @parents;
- return @parents, map { get_all_parents( $_, $_ ) } @parents;
+ return @previous unless @parents;
+ return @parents;
}
sub super()


You should mention Moose, which provides a Perl 6 inspired declarative sugar layer over Class::MOP.
Yuval, I'd hate to ruin the surprise of what I shall review next!
Regarding benchmarking: A couple of use benchmarked a replacement of Class::ISA::super_path with Class::MOP's "class_precendence_list". In these casual tests, Class::MOP was 25 to 50% slower.
I agree Class::MOP is clean and nice, but I won't be switching performance-sensitive code over to use it just yet.
Thanks for the data point, Mark. I talked to Stevan about this last week at YAPC::NA, and he has some strategies to optimize the code.
I also offered to write a few parts in XS, if that would help. So far, his goal has been completeness and accuracy instead of speed. Hopefully convenience is possible without too much penalty.