A Metaobject Protocol for Core Perl 5 (translated from Russian)
A Metaobject Protocol for Core Perl 5
(source document: http://habrahabr.ru/post/198274/)
The idea of creating a MOP for Perl 5 has been floating around for quite some time. One well known implementation - Class::MOP - is used in Moose. But to have a chance of getting into the core, the MOP must be compatible with the existing object model and unladen by excessive features or dependencies.
Stevan Little recently published the first experimental release of just such a one: the mop module. The project has undergone a long evolution with a good deal of attention from the community throughout. Let's take a look at what turned out, and what that might mean for Perl 5.
What's a MOP?
A Metaobject Protocol is an API for controlling the OO system in a language. Just as an object is an instance of a class, the class itself is presented as an object (or metaobject) with programmatically determined properties (attributes, methods, etc.). In explaining Moose to his mom, Stevan Little put it thus:
“It's an abstraction for a system of abstractions, used for the creation of abstractions.”
When it comes to messing with classes in core Perl, we've all encountered the simplest implementation of a MOP:
no strict 'refs';
*{$foo . '::bar'} = \&baz;
This crypto-gram adds a method 'bar' to a class indicated in the variable $foo at runtime by using a link (or alias) to the function baz. If anybody is familiar with the term 'reflections', then this can be seen as an example of that. The difference between MOP and reflection lies in the fact that MOP provides a convenient and flexible tool for metaprogramming classes without inevitably using dubiously legal hacker techniques. For example:
class Foo {
method bar {
baz
}
}
Why is MOP needed in the core distribution of Perl?
The classic work on metaobject protocols is "The Art of the Metaobject Protocol", which describes the implementation of the metaprotocol in CLOS (Common Lisp Object System). One of the core requirements of CLOS's creation was to solve the problem of the multitude of mutually-incompatible object system implementations for Lisp. The metaprotocol enabled the construction of interoperable, flexible, and extensible object systems which satisfied all the demands of Lisp programmers.
A simple and unencumbered (in dependency terms) implementation of a MOP in core Perl could singlehandedly answer the question of choosing object systems when starting a program. It will conclude all the religious wars comparing OOP frameworks and turn the inclusion of a MOP into a habit like use strict.
A MOP implementation for core Perl should be light and fairly quick -- with these criteria, Moose will never make it, but mop still has a chance.
A logical and syntactically beautiful object system could inspire rewrites of many modules whose authors decided against using an OOP framework due to bulk or excessive dependencies. This would (in turn) significantly improve the readability of code and lessen the maintenance burden via (for example) a more unified style and simplified static code analysis.
Naturally this could attract younger developers to Perl -- simplicity and brevity are extremely important for mastering any language calling itself 'object oriented'.
As several people in the community have noted, it's entirely likely that MOP will become an important part of the future of Perl, and create a sharp boundary in Perl's history: before core MOP and after.
Evolution of mop
mop’s development is preceded by a long road. After participating in the development of the Perl 6 object system under Pugs, Stevan Little decided to bring those lessons over to Perl 5. After a few attempts, Class::MOP appeared and became the basis for the creation of Moose. Moose became very popular -- it gave developers the tool for working with classes that they had awaited for so long. But Moose's long startup time and large dependency tree scared potential users. Thus, two years ago, Stevan started chasing the idea of creating a compact MOP system which would go into the core Perl distribution. This was p5-mop, but it ended up collapsing under the weight of its own complexity, which discouraged Stevan and led to an unexpected experiment: Moe, a Perl 5 compiler using Scala.
Some time passed, and, deciding that p5-mop needed a second chance after all, Stevan founded p5-mop-redux, which avoided the quixotic quest of implementing all of Moose’s features. Instead, it aimed to simply be the base for an expanded object system in core Perl.
Stevan consistently kept the community informed about the progress of his work, and published articles on the blogs.perl.org platform. On October 15th, the first experimental release of the module was published to CPAN. Now there are two active developers: Stevan Little and Jesse Luehrs, as well as a few contributors.
Working with mop
cpanm can be used to install mop:
$ cpanm --dev mop
Note that mop actively uses new features of Perl, such as the Perl parser API. The lowest version of Perl that the module supports is 5.16.
First example
use mop;
class Point {
has $!x is ro = 0;
has $!y is ro = 0;
method clear {
($!x, $!y) = (0,0);
}
}
A class is defined with the keyword class. We first declare the Point class, in which attributes are defined with the help of the has keyword. Note that variable attributes are distinguished from regular variables with the help of twigils. This is almost a complete copy of Perl 6 syntax. At the moment only attributes with the $! twigil are supported, and they behave as public attributes (even though in Perl 6 they indicate private attributes). (translator's note: they actually behave the same as Perl 6: they are private to the class, not visible to outsiders or subclasses)
Following the twigil definition comes the description of traits after the keyword is. For example, ro indicates that the attribute is accessible only for reading. After the equals sign we see the default value of the attribute. The Point class has only one method: clear, which throws out the values of the attributes. It’s evident that the variable attributes $!x and $!y are accessible inside methods as lexical variables in the scope of the given class. Their values are isolated to the boundaries of each instance of the class.
class Point3D extends Point {
has $!z is ro = 0;
method clear {
$self->next::method;
$!z = 0
}
}
Here we see a class definition for Point3D, which inherits from the Point class with the help of the keyword extends. Thus, the child class adopts all attributes and methods of the Point class. Point3D adds the $!z attribute to these. Additional, it redefines the clear method: as is evident from the code, it uses next::method to make a call to the clear method of the next (in hierarchical inheritance terms) parent class, in this cass Point. Additionally, every method automatically defines the variable $self, which is a reference to the current object.
In the case where no parent class is indicated for inheritance, a class will inherit by default from the mop::object class. This is demonstrated in the following example:
print Dumper mro::get_linear_isa(ref Point->new);
$VAR1 = [
'Point',
'mop::object'
];
Object attributes may be defined during instantiation:
my $point = Point->new( x => 1, y => 1);
Note that we did not define the new method for the Point class. This method was inherited from the mop::object class.
Getter methods for accessing attribute values are automatically created:
my $point = Point->new( x => 1, y => 1);
print $point->x
The output will be 1. Given that the attribute was declared as ro, an attempt to modify it will result in a runtime error:
$point->x(2);
Cannot assign to a read-only accessor
Nevertheless we can freely modify attributes inside of methods: for example, we can create a setter method set_x:
class Point {
has $!x is ro = 0;
...
method set_x( $x=10 ) {
$!x = $x
}
}
This example dramatically displays how to define a method signature; that is, describe the argument variables passed into the method, and even define default values if the argument is omitted.
$point->set_x(5); # $!x is now 5
$point->set_x; # $!x is now 10$
At the same time attributes are in scope only for the Point class; that is, we can’t directly operate on them in descendant classes.
class Point3D extends Point{
...
method set_x_broken {
$!x = 10;
}
}
This gives a compilation error: No such twigil variable $!x.
Roles
Roles enable flexibly composing classes with the required methods and attributes, allowing you to avoid the majority of inheritance.
role BlackJack {
method win;
method lose ($value) {
not $self->win($value)
}
}
The given role defines two methods: win and lose. The win method has no body, which means the method must be defined in the internals of the class that uses it. In this sense a role is similar to the interface found in other programming languages.
Now you can compose a class with the given role using the keyword with:
class LunaPark with BlackJack {
method win ($value){
0
}
}
A class can be composed out of several roles, in which case role names are separated with a comma.
Attribute properties and values
has $!foo is rw, lazy = 0
Attribute properties are listed with commas after the keyword is. At this time the follow properties are supported:
ro/rw -- read only access / read/write access
lazy -- the attribute is created on first access
weak_ref -- the attribute is a weak reference.
When defining default values it’s important to remember that a construction is carried out after the equals sign, so this line:
has $!foo = "value"
means:
has $!foo = sub { "value" }
That is, this is in fact a function for creating a value, and not an evaluation of the given expression.
When defining a default value you can link to the current object by using the $_ variable:
has $!foo = $_->set_foo()
If you want a particular attribute to be required by the object’s constructor new, you can provide a default value like this:
has $!foo = die '$!foo is required';
Accordingly, an undefined foo attribute during the object’s construction will raise an exception.
In order to avoid overburdening mop’s core with excessive complexity, type, class, attribute, and method checking were not implemented. However, such checks can be easily implemented by an external function:
sub type {
# function to check types
...
}
class foo {
has $!bar is rw, type('Int');
method baz ($a, $b) is type('Int', 'Int') {
...
}
}
A working implementation of this function can be found in an example for the mop module.
Module creation
A typical module file might look like this:
package Figures;
use strict;
use warnings;
use mop;
our $debug = 1;
sub debug {
print STDERR "@_\n" if $debug;
}
class Point {
has $!data is ro;
method draw_point {
debug("draw point")
}
method BUILD {
$!data = "some data";
}
method DEMOLISH {
undef $!data;
}
}
class Point3D extends Figures::Point {}
my $point = Figures::Point3D->new;
$point->draw_point;
As is evident from the example, classes will receive the appropriate prefix if a package is defined. Moreover, classes receive access to the module scope. This means that functions and variables defined in the module scope are also accessible for use inside classes. In this case such functions do not become class methods. There is no other namespace pollution by exported functions!
The special BUILD method can be used if object construction requires some particular initialization. This is convenient and allows skipping redefining the new method.
The DEMOLISH method is called with the object is destroyed: it’s a destructor.
Internal layout of mop objects
An object created by mop is not the familiar (to many) blessed reference to a hash. Instead, mop uses the so-called InsideOut objects, where the entire internal structure is hidden in the code of the class and accessible only through special methods. There are a few public methods in mop which allow you to inspect the internal structure of an object:
mop::dump_object - dump an object
print Dumper mop::dump_object( Point3D->new )
$VAR1 = {
'$!y' => 0,
'CLASS' => 'Point3D',
'$!x' => 0,
'ID' => 10804592,
'$!z' => 0,
'SELF' => bless( do{\(my $o = undef)}, 'Point3D' )
};
mop::id - an object’s unique identifier
print mop::id(Point3D->new)
10804592
mop::meta - meta information about the object for detailed introspection.
print Dumper mop::dump_object( mop::meta( Point3D->new ) )
$VAR1 = {
'$!name' => 'Point3D',
...
# a very long sheet with summaries of all object characteristics
...
}
mop::is_mop_object -- boolean function, returns true if the object is a mop::object.
print mop::is_mop_object( Point3D->new )
1
Practical Usage
The mop module is currently undergoing active testing by the community. The base recommendation is: take your favorite module and try to rewrite it using mop. What mistakes or problems do you end up dealing with? Write about it -- it’ll really help with the further development of the project. For example, Plack was successfully ported, passing all 1152 tests.
At the moment it’s difficult to say whether mop will be included in the base Perl distribution. If it’s included, will it be with 5.20, 5.22, or later still? Nobody knows, but the extremely positive general vibe of the project is inspiring.
Thanks for putting in the effort to do this, btyler.
A very smooth translation. Well done!
Good description of mop!
> so I hacked up a translation of vsespb's last post.
note, that I am not the author, I just posted a link here
Thanks, updated the attribution.
Actually I have one correction:
The attributes are all private, they are not visible outside of the class or even in subclasses, which is exactly how Perl 6 works too.
Thanks Stevan - added a note to that effect (and corrected the spelling of your name -- sorry!)
Thank you for doing this: it's an interesting and informative read, and your translation skills are excellent.