Published on 2010-11-08
Discovering Metaobject Protocols
Metaobject protocols (short MOP) are APIs that control how objects, classes and other object-oriented things work.
If you're not satisfied with such a short explanation, or want to see a MOP developed and used, read on.
A Simple MOP: per-instance methods
Suppose you write a software that works with lots of objects which mostly work the same, but some have special abilities or behavior. You don't want to write a different class for each special object. An alternative solution could be to allow adding or overriding methods per instance.
Here's an example of how such a thing could work in Perl 5:
use strict; use warnings; # just needed for say(); use 5.010; { package A; sub new { my $class = shift; bless {}, $class; }; # install a new method per instance sub override_method { my ($self, $name, $meth) = @_; $self->{__methods}{$name} = $meth; } sub callmethod { my $self = shift; my $name = shift; # use per-instance method if available if (exists $self->{__methods}{$name}) { goto &{$self->{__methods}{$name}}; } else { # fall back to normal dispatch otherwise $self->$name(@_); } } # a dummy method for testing sub dummy { say "dummy: $_[1]"; } } my $o = A->new(); $o->callmethod('dummy', 42); $o->override_method('dummy', sub { say "dummy overridden" }); $o->callmethod('dummy', 42); A->new->callmethod('dummy', 42);
The output is
dummy: 42 dummy overridden dummy: 42
This code introduces a new way to do method calls: instead of
$obj->name(arguments)
you now do
$obj->callmethod('name', arguments)
. The
callmethod
method looks into a per-instance hash if an overridden
method is defined; if yes, it calls the overridden method. If not, it uses the
normal method dispatch as a fallback.
The new method call syntax could be avoided by some clever tricks, but that really doesn't matter for the description of metaobject protocols.
What matters is that with some ordinary Perl code we could implement a new object-oriented feature.
To make this customization reusable, callmethod
and
override_method
can be put into a separate class (let's call it
Method::PerInstance
), and any class that wants to use it just
needs to inherit from it.
We call this class a metaclass. class because it is a
class, and meta because it controls classes again. In fact, you can
use it control itself, and for example override the
override_method
in an instance of the
Method::PerInstance
class.
Types of MOPs
The Method::PerInstance
metaclass controls only method
dispatch. That's a very interesting topic, but certainly not the only one that
a metaclass can manipulate. Others include
- Attributes, i.e. per-instance storage space
- Introspection, i.e. asking a class for its methods, attributes, parents and other data
- Instantiation, i.e. object construction
Prior Art
If you design a new programming language, or a new meta object system, you should read The Art of the Metaobject Protocol (referral link). It is the number one resource about MOPs.
If you just use a programming language, you usually shouldn't invent your own MOP, but use an existing solution.
For Perl 5, Class::MOP is probably the most popular and most complete metaclass protocol. Moose, the popular, post-modern object system for Perl 5 builds on it.
It was inspired by the development of the Perl 6 programming language, so Moose and the Perl 6 MOP share lots of ideas.
A Metaclass in action
If you write a Perl 6 program like this:
class A { method greet($whom = 'world') { say "Hello, $whom!" } } A.new.greet('world');
The compiler will run roughly this code
# at compile time: my $class = ClassHOW.new_class('A'); $class.^add_method('greet', method ($whom) { say "Hello, $whom!" }); $class.^compose; # and at run time: $class.new.greet('world');
... except that it will install the class into the symbol table, instead of
storing it in variable $class
.
ClassHOW
is the name of the meta class for classes (as opposed
to roles and grammars). HOW stands for High Order Workings, or else
describes how a class does its work. The caret in .^add_method
and .^compose
means it's a call not to the class itself, but to
the metaclass.
The call to .^compose
is necessary because in general, some
calculations need to be done when a class is fully built (for example if
multiple roles are mixed in, method name conflicts need to be resolved).
But not only the compiler makes good use of the metaclass model; you as a programmer can do plenty of nifty stuff. Here's an example that automatically adds a wrapper to methods, and logs the nameof the called methods to STDERR:
sub log-calls($obj, Role $r) { my $wrapper = RoleHOW.new; for $r.^methods -> $m { $wrapper.^add_method($m.name, method (|$c) { # print logging information # note() writes to standard error note ">> $m"; # call the next method of the same name, # with the same arguments nextsame; }); } $wrapper.^compose(); $obj does $wrapper; } role Greet { method greet($x) { say "hello, $x"; } } class SomeGreeter does Greet { method LOLGREET($x) { say "OH HAI "~ uc $x; } } my $o = log-calls(SomeGreeter.new, Greet); $o.greet('you'); $o.LOLGREET('u');
Output:
>> greet hello, you OH HAI U
The sub log-calls
(taken from DBDI)
takes an object and a role. It wraps all methods from the role with a simple
logging facility, and prints the method name to the standard error stream.
It uses introspection to find the methods, and programmatic method adding for the wrapping. Both are typical metaclass features, and are put to good use for automatic logging.
Literature
Literature on meta object protocols seems to be quite sparse. Regarding books there is The Art of the Metaobject Protocol by Kiczales, Des Rivieres and Bobrow. It talks about the meta object protocol developed for Common Lisp, and is well worth reading.
The Class::MOP documentation has a nice section on literature, which holds a few more references.