layout | title | author | categories | tags | date | image |
---|---|---|---|---|---|---|
post |
Metaobjects in the Cor |
Chris Prather |
oo, perl, mop |
Fri Apr 3 13:06:07 CDT 2020 |
Back in Oct 16, 2019 Ovid published his proposal for a core object system for Perl code-named Cor. I've been thinking about how to respond ever since, but life has gotten in the way time and again.
The Proposal that Ovid makes is good on it's surface. It's very good in a lot of ways, and based on feedback from various parts of the community it's gotten better. The places I have disagreements with the syntax all fall into "work in progress" categories. Not that my opinion really matters.
An example of the syntax (as it currently stands):
class Cache::LRU {
use Hash::Ordered;
has $cache :private :handles(get) = Hash::Ordered->new;
has $created :private :reader :immediate = time;
has $max_size :optional :reader :isa(PositiveInt) = 20;
method set ( $key, $value ) {
if ( $cache->exists($key) ) {
$cache->delete($key);
}
elsif ( $cache->keys > $max_size ) {
$cache->shift;
}
$cache->set( $key, $value ); # new values in front
}
}
The biggest change Moose brought to Object Oriented Programming in Perl was to make Attributes a first class member of the object system. Before Moose, Attribute data was a side effect of the object representation and whatever accessors the developer happened to decide to provide (if any). With Moose the idea of what state an object contained stepped to the foreground.
Cor continues this, but takes it a step further. One of the complaints about
moose is that it exposes too much automatically in the
objects API. Moose's has
function exports too much by default. Cor picks
better defaults for accessor and constructor behavior and learn the lessons
we've learned from the [M-star]1 collection of object systems on CPAN.
But Cor is missing the second biggest advancement Moose brought to Perl, The Metaobject Protocol.
From Wikipedia:
A metaobject protocol (MOP) provides the vocabulary (protocol) to access
and manipulate the structure and behaviour of systems of objects.
Moose has a complex set of objects and classes that allow you to override and extend nearly every piece of it's infrastructure. So many objects and classes that people started trying to ditch them, leading to the creation of Moo2. The Cor propoasal so far has left the MOP entirely undefined.
Without a Meta-Object Protocol, any object system in Perl's core won't be revolutionary, and will most likely be doomed to failure.
Paul "LeoNerd" Evans has started a proof of concept project influenced by the Cor proposal, Object::PAD. Technically it's a proof of concept for an Object system where the attribute slots are lexical variables, but he's leveraging the syntax work that Ovid has done with Cor to proceed.
One of the bugs that he ran into turns out to be a bug in standard Perl too.
# DO NOT USE THIS BUGGY CODE
package Parent {
sub new ($args) {
my $self = bless $args, __PACKAGE__;
$self->init()
return $self;
}
sub init ($self) { }
}
package Child {
use parent 'Parent';
sub new($args) {
my $self = shift->SUPER::new($args);
$self->{foo} //= "bar";
}
sub init ($self) {
warn "the value of foo is: $self->{foo}";
}
}
Child->new()
In production this subtle bug (did you spot it?) will trigger an undefined
warning when it tries to run Child::init
before populating $self->{foo}
. Or
if you have warnings as fatal (because say you're trying to access a
non-existant lexical slot) it'll blow up.
In Java for example, during object construction any method calls (like
$self->init()
) are dispatched on the parent class until the child class has
finished being constucted.
Perl has had Object Oriented Programming for over 20 years with an ad-hoc
MOP.3 Object construction is simple: bless $data, $class
. Classes are just
Packages, so if you want to manipulate a class dynamically just manipulate the
package's symbol table.
# DEMO CODE DO NOT USE
package Student {
sub new ($args) {
die unless exists $args->{id};
return bless $args, __PACKAGE__
}
# create some accessors
for my $method (qw(name id grade teacher)) {
no strict "refs";
*Student::$method = sub ($self, $value) {
if (defined $value) {
$self->{$method} = $value;
}
$self->{$method}
}
}
}
This has the advantage of being a very flexible system.[^4] But it relegates attributes to be almost a side-effect of objects having state, and requires a lot of manual work on the part of the project developer to ensure that the object system is correct and consistent. Separating those concerns out is the process of defining a Meta-Object Protocol.
Stevan Little has givena
fewtalks about building MOPs for Perl. (That
list isn't exhaustive, it's just the top 3 I found on YouTube). His most recent
works are UNIVERSAL::Object
and MOP
.
UNIVERSAL::Object
defines a very basic MOP for objects in
Perl. It adds attributes as a first class concept, not as a side-effect of
object state. Because it defines an initialization order (via BUILD
methods)
in the MOP you can avoid the $self->init()
bug we saw earlier.
# THIS CODE IS SAFE BUT USELESS
package Parent {
use UNIVERSAL::Object;
}
package Child {
use parent 'Parent';
our %HAS = (
%Parent::HAS,
foo = sub { "bar" },
);
sub BUILD ($self) {
warn "the value of foo is: $self->{foo}";
}
}
Child->new()
We give up the flexiblity of writing our own constructor, but we get back consistency and a defined order of operations during object construction.
MOP
expands on this to include primarily introspection and Role
composition.
Perl has never had explicit layers like this. Even XS is really just a series of macros that "safely" expose parts of the Perl core.
##All Is Lost: MOPs are a performance and maintenance overhead
Adding a MOP means adding a lot to the Cor proposal. Not only do all the syntax changes have to be incorporated, but lots of new protocols have to be defined, vetted, and then later maintained.
The banner image is Longing by CaseyWest, on Flickr.
[^4] Give or take the bugs outlined in the comments in [Package::Stash::PP][package-stash]. Or the fact that Package::Stash::XS is the preferred version for correctness.
Footnotes
-
Moose, Moo, Mouse, Mojo::Object etc. All attempt to use similar semantics with various levels of support. Hence
M*
. ↩ -
Moo has just enough meta to make things work and if you want more than that it goes and loads Moose anyway. ↩
-
All object systems have a Metaobject Protocol. Some just have an explicit MOP while others have an implicit system. Perl's ad-hock system of bless(), and Package stashes forms a MOP, just not a very well defined one. ↩