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.