2009-05-17

Moose's Metaobject Protocol: Foray

To avoid frivolous toy examples, this series of articles draws heavily from Perl's excellent DateTime module. You really should be using this module, but even if you aren't, it has a very clear API so you should have no trouble.

Breaking in

If you use Moose, you've probably had it drilled into your head that you always need to "make immutable". Maybe you don't know what it does or means (beyond that it makes your code faster), but you've written it how many times now?

__PACKAGE__->meta->make_immutable;

Just what the hell is __PACKAGE__->meta (a.k.a. DateTime->meta1) anyway? Well, print it out (or inspect it with Devel::REPL as I do here).

$ __PACKAGE__->meta
Moose::Meta::Class=HASH(0x9021fc)

So DateTime->meta is an object of class Moose::Meta::Class. Moose::Meta::Class is, unsurprisingly, a metaclass.

A metaclass is a class for classes. Every class is an instance of a metaclass.

Breathe in. Breathe out.

In the exact same way that objects are instances of classes, classes are instances of metaclasses. Metaclasses are classes. Classes are objects.

To be clearer, here's a table of classes, and instances of those classes.

In the end, it's all just object-oriented programming. You lot are quite adept at OOP, so you have nothing to fear! I promise I'll go slowly.

So we have this Moose::Meta::Class object. Let's call some methods to see else this unfamiliar beast knows.

> DateTime->meta->superclasses
Moose::Object

> DateTime->meta->subclasses
$ARRAY1 = [
            'DateTime::Infinite::Future',
            'DateTime::Infinite::Past',
            'DateTime::Infinite'
          ];

> DateTime->meta->get_all_method_names
$ARRAY1 = [
            'compare',
            '_calc_utc_components',
            'date',
            # ... elided ~173 methods ...
            'time_zone',
            'subtract_duration',
            'mjd'
          ];

Interrogation

As you can see, metaclasses hold a treasure trove of information about a class. You probably already know how to get the superclasses of any Perl class, but how the hell did Moose know what the subclasses of DateTime are?2 On second thought, who cares! This is simple encapsulation, a familiar principle of OOP. The relevant question is not how? but what?

Consider the problem of getting the names of all methods DateTime accepts 3, using just vanilla Perl OO. You have to grab a reference to DateTime's symbol table, iterate over the key-value pairs, plucking out the name of any typeglob with a CODE entry. All told, it ends up looking like this:

my $symbol_table = \%DateTime::;
for my $name (keys %$symbol_table) {
    my $glob = $symbol_table->{$name};
    next unless *{$glob}{CODE};
    say $name;
}

Generalize that to work for any class. Oh, and make it crawl up the inheritance hierarchy, since an object can accept its superclasses methods. Now you've got a seriously hairy piece of code. I'd much rather stick with __PACKAGE__->meta->get_all_method_names. Of course, you need Moose to use this nice API...

I have a confession to make. DateTime is not written with Moose at all.4 It's written in plain Perl OO. To interrogate DateTime for its inheritance hierarchy and methods, I didn't have to port it to Moose, either. Moose will inspect plain Perl classes to pull out as much information as it can. In fact, Class::MOP (Moose's foundation) has something very close to that hairy bit of code in it. Class::MOP's symbol table crawling is written in XS, so it's also faster than the Perl equivalent.

Though Moose is quite happy to work with vanilla Perl classes, the metaclass will have plenty more information if the class was written with Moose. Since vanilla Perl has no notion of attributes — just methods that poke into the instance's data — Moose can't learn anything about the attributes of a vanilla class.


Metaclasses have plenty more to offer, but we need more context. Next time, we'll look at what other objects live in the Moose ecosystem. In the meantime, snoop around some of your classes' metaclasses.5 If you want to inspect a class that doesn't use Moose, you can use the following to generate a metaclass for your class:

use Your::Module;
use Moose ();
my $meta = Moose::Meta::Class->initialize('Your::Module');
print join ', ', $meta->superclasses;

Footnotes

  1. This is a bit of a fib. DateTime has no ->meta method. I'll explain what's up soon! (back)
  2. The builtin mro::get_isarev function (and its MRO::Compat analogue for 5.8) is exactly for this purpose. In 5.8, every package must be searched, but in modern perls it is cached. (back)
  3. There are several reasons you might want to inspect a class's methods. The foremost use, and probably most nefarious, is to look at what methods you can call when you're using that particular class. You should read its documentation instead! A better use is in making sure you're not accidentally overriding any methods from a subclass. (back)
  4. Yet! There are vague mumbles of a possible DateTime 2 that would probably use Moose. Dave Rolsky is a core Moose developer, after all. (back)
  5. If you have no classes handy, you can earn bonus points by poking at Moose::Meta::Class's metaclass, or Moose::Meta::Class's metaclass's metaclass! (back)

3 comments:

zby said...

I guess this question could be only justified by the excellence (bordering on the area of magic for me) of other parts of Moose - but why this "__PACKAGE__->meta->make_immutable;" is not yet made a default?

Shawn M Moore said...

zby, make_immutable is not the default because we simply don't know when you're done building your class. We have no useful hook for "falling off the file". There is B::Hooks::EndOfScope, but that is a compile-time hook. At the end of your class's compile-time, the only entities that have been created are subroutines. Class-building functions like "has", method modifiers, and even "extends" all take effect at runtime.

"no Moose" occurs during compile time (just like "use Moose" does) so we can't make_immutable at "no Moose" time.

I'm sure this problem can be overcome, since Moose as of a few versions ago unconditionally depends on XS.

MooseX::Declare can get away with making make_immutable the default, because it does have such a hook (which is "when the class-generator coderef returns").

Matt said...

I think between B::Hooks::EndOfScope and B::Hooks::OP::Check::StashChange we could do this pretty well - you'd use them to figure out when your file finishes or changes package and then use Devel::Declare to inject a lump of code that does the immutable call.

I'm not entirely convinced this level of magic is worth it outside of something like MooseX::Declare though.

Post a Comment