A Meta Object Protocol for Perl 5
This software is copyright (c) 2017, 2018 by Stevan Little.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
A Meta Object Protocol for Perl 5
There is a need to have a name which means both "role" and "class".
This is useful for methods like the origin_class
method of mop::attribute and mop::method.
Suggestions:
Hi Stevan,
I am posting this on the MOP
repo - lack of a better place for these kind of general topics.
It started out as an enhancement request for the roles
pragma and ended up becoming a general note about the MOP
and the slots
roles
pragmas,
as well as the @DOES
and %HAS
conventions (suggesting 2 similar symmetrical conventions along the way: @HAS
and %DOES
).
It's quite loooong. And it might contain a high amount of gibberish. My apologies if that's the case.
@DOES
and %HAS
conventions ..Having a mechanism through which a package
(role, class, whatever) can simply state the roles it wishes to do, without bothering with the details of how they are composed, is very interesting.
And one way of achieving that is to stuff those wishes in an array (like @DOES
), in a way akin to @ISA
.
Same goes for the ability to declare a slot by just putting some stuff in a package variable %HAS
(or @HAS
, see below).
I think this an excellent idea... And I hope to convince you to stop seeing these as just "surface features" of the new MOP and the slots
/ roles
pragmas, as mentioned in 4.
If well specified/documented, those package variables have got the potential to become a glue/wormhole between the living quarters of Flintstones
and the Jettsons
and anything in between...
-- as simple conventions that can be followed by any current or future MOP / role-composer, including the popular Role::Tiny
, the Cor
project, and perhaps even the Mo*
family at some point...
That sort of thing is well served being "low-tech"... And that's the beauty!
The fact that roles are not currently implemented in core is mostly irrelevant, I believe.
So is the "hijacking" of the package symbols --which can be a problem, yes... but not in any way different than what it would be if it were done by core...
There do appear to be a few hurdles on that pathway, though...
@ISA
@ISA
is normally pure data;@ISA
doesn't itself contain any "dirty state" of the inheritance operation or method dispatch. Caching and other dirt occurs elsewhere.@ISA
... use parent
being the preferred one these days... Normally, it doesn't matter who populated it or how, as long as it is there when it is needed (method dispatch).@ISA
is not typically the code that implements any form of inheritance or method dispatch. The base
pragma partially violated this (by entangling itself with %fields
and pseudo-hashes). Remember what happened later?roles
, slots
and the MOP
In a nutshell, I think the roles
and slots
pragmas are doing too much and the MOP
commits a sin by reading from and writing to the same place, namely %HAS
:-)
roles
pragma does too much because it combines (and tightly couples) the declaration and composition of roles.slots
pragma, which goes beyond simply stuffing the caller's wishes somewhere, but actually schedules slot inheritance.Also, both the roles
slots
pragmas (well, actually the MOP
) commit a sin by stuffing the dirty state (i.e. one of the outcomes) of role composition within the %HAS
hash (which also serves as their INPUT
)
I am not sure if there is an existing module that commits an equivalent "sin" for @ISA
. The analogy would be something like mro
module rewriting @ISA
at UNITCHECK
time and stuffing the equivalent of get_linear_isa()
in there...
Here's an alternative way of dealing with the above (which you might have already considered and ditched; if so, I would really like to hear the reasoning), which entails hijacking 4 package variables (instead of 2) though:
@HAS
==> merged to %HAS
(during successfull composition)@DOES
==> merged to %DOES
(during successfull composition)This may sound complex, but I think it actually results in something simpler, and does away with some of the circularities, both conceptually and implementation-wise. Just bare with me, please.
The array variables (@HAS
and @DOES
) would be where "local" wishes are recorded by mere mortals (or thru dumbed down versions of the slots
and roles
pragmas, as described below), meaning ==> @DOES
would keep its current semantics.
The hash variables (%HAS
and %DOES
) would be where the related claims are placed, typically merged in via role composition (but not necessarily), meaning ==> %HAS
would keep its current semantics.
The suggested %DOES
convention (where role claims would be placed) is more of a nice-to-have, but its presence provides a symmetrical way to present things, also absorbing the gist of Class::DOES
along the way.
The slots
and roles
pragmas become even simpler than they already are, they would just be stuffing things in @HAS
/ @DOES
respectively (similar to what the parents
pragma does to @ISA
), without doing/scheduling any composition.
A conforming composer (like MOP
and possibly Role::Tiny
and Object::Pad
) would just look at @HAS
and @DOES
in order to gather the relevant requests, then do their thing, and then merge the resulting successfull claims in %HAS
and %DOES
.
Any other conforming composer (like
Role::Tiny
, orObject::Pad
, if they wish to conform) would also be able to merge things in%HAS
or%DOES
. This could also include mere mortals (at their own risk)
DOES()
method becomes trivial (wherever it is implemented): it would only have to look at %DOES
and just deal with the @ISA
interplay. It would need no services from the MOP
(except perhaps a utility function for gentle stash access, currently in MOP::Internal::Util
).And the whole thing is pretty much compatible with the current state of affairs (in terms of API) and requires minimal to no changes to UNIVERSAL::Object
(the more stable sister of the bunch), depending on the way you look at it.
It comes with a small nuisance though:
Participating classes and roles (those adding slots or wishing to do other roles) would need to use
an additional module, which I nicknamed Composer
below (just made up the name, it's probably not the best).
Composer
, also quite a brief, is where role/slot composition would be scheduled (instead of from within the slots
and roles
pragmas). If/when the MOP makes it to core
it may become a no-op.
What do we gain from this nonsense?
In terms of direct functionality gains: not much -- except for a future possibility to reconstruct slot definition order via @HAS
, which may become handy at one point.
The main advantages come from a clear separation of concepts and concerns, and a potential for interop going forward.
In this paradigm:
The @HAS
and @DOES
conventions can be independently specified/documented, focusing only on semantics. They become true declarations of "local" wishes / requests.
This is very similar to the case of @ISA
.
The dumbed down versions of slots
and roles
pragmas can be used anywhere without any implied entanglement with a any given implementation, just like the parent
pragma.
They also become the perfect places to document and maintain the @HAS
and @DOES
conventions.
You also get quasi immediate and solid behavioral stability for the slots
and roles
pragmas. They become so dumb and boring so as to deserve v1.0 on a fast-track.
-- otherwise, people would possibly sue them in the future for name-squatting :-)
The %HAS
and %DOES
variables are where things are reported / claimed (as opposed to simply being requested).
For example, in addition to "local" slots coming from @HAS
, the inherited/composed slots also find their way into %HAS
.
Likewise, the %DOES
hash would contain all the roles done by a given package (including those composed through roles that do other roles), but not necessarily those done by its ancestors.
With these small adjustments, it becomes very easy to swap composers at will (and experiment with new ones) without reinventing too many wheels and new conventions.
The MOP
remains to be a passive light-weight toolbox/library with no state of its own (like today).
A Composer
is an active thingy: it takes initiative (if used
). It uses a MOP
to do its thing. Going forward, there may be different implementations for each.
The standard Composer
(or alternate composers) do not really need to expose much of an API distinct from what is described.
Almost no one really needs to call upon their specialized methods:
@HAS
and @DOES
) with documented semantics.%HAS
and %DOES
)This should also make it easier to include this stuff into the core. Just one hook on UNITCHECK
.
Possibly, some run-time meta-dynamism?... Not sure.
In any case, if we want that, we need to make sure that a successful compose_roles()
operation can be repeated without issues .
Enough chatter. Here's some CODE, which should be much clearer than the long description above.
(just to show intent. didn't even check if these compile)
package Point;
use Composer; # May become a no-op if MOP makes it in core.
use parent qw/UNIVERSAL::Object/;
use roles qw(Geometric);
use slots (
x => sub {0},
y => sub {0}
);
package Point3D;
use Composer::Some::Alternative; # Allow alternative composers and experimentation.
use parent -norequire, qw/Point/;
use slots (
z => sub {0},
);
slots
pragmaThe slots
pragma becomes, in essence:
package slots;
sub import {
shift;
my $pkg = caller(0);
{
no strict 'refs';
push @{"$pkg\::HAS"}, @_;
}
}
roles
pragmaThe roles
pragma itself would become pure, void of any knowledge of how/when or even by whom a role will eventually get composed.
In essence:
package roles;
use Module::Runtime ();
sub import {
shift;
my $pkg = caller(0);
Module::Runtime::use_package_optimistically( $_ ) for @_;
{
no strict 'refs';
push @{"$pkg\::DOES"}, @_;
}
}
DOES()
methodThe default DOES()
method becomes (wherever it is implemented) :
sub DOES {
my ($self, $role) = @_;
# get the class ...
my $class = ref $self || $self;
# if we inherit from this, we are good ...
return 1 if $class->isa( $role );
# The suggested %DOES convention.
# This is not absolutely necessary, but just makes things
# simpler and potentially interopable between different MOPs.
# - TABULON
{
no strict 'refs';
require mro;
for my $pkg ( $class, mro::get_linear_isa() ) {
# Not exactly sure about the interpretation of a false value for a role.
# The below interprets it as an explicit claim for NOT doing a ROLE.
# FIXME::may need to access more gently or just go thru the MOP.
return ${"$pkg\::DOES"}{ $role } // 1 if exists ${"$pkg\::DOES"}{ $role };
}
}
return 0;
}
Note that, in terms of behavior, the above %DOES
convention is almost identical and should be interoperable with Class::DOES
. The only difference is the interpretation of falsy values in the %DOES
hash:
Composer
moduleThis is where things are actively tied together, but it's also dead simple (thanks to the MOP doing all the hard work).
package Composer;
use MOP ();
sub import {
shift;
my $pkg = caller(0);
MOP::Util::defer_until_UNITCHECK(sub {
my $meta = MOP::Util::get_meta( $pkg );
MOP::Util::inherit_slots( $meta ); # would we still need this line ?
MOP::Util::compose_roles( $meta );
});
}
OptList
as arguments for slots
and roles
pragmas ?The slots
pragma already accepts key-value pairs. It might be interesting to consider accepting an OptList
as well (in the Data::OptList
sense, but not necessarily depending on it).
A similar consideration applies to the roles
pragma which currently accepts a flat list of role names. An upgrade to OptList
looks like a natural fit there, once we start defining (or better yet, stealing) a sub-syntax for certain options that may be needed for resolving conflicts during role composition (exclusion, renaming, aliasing, ...).
I am not that sure about the form of the actual contents of @HAS
and @DOES
, though...
Basically we would want to avoid alternate composers from stepping on each others toes, like in the case below :
use Composer;
use Composer::Other;
use slots ...;
use roles ...;
It might be possible to tackle this by having a Composer
implementation mark its name within yet another package hash.... or export a subroutine or something... Need to think this through.
In any case, it smells like a stairway to... METACLASS()... :-)
There are some other open questions and points worth raising. But this post is already veeeery long...
Installation instructions:
git clone https://github.com/stevan/p5-MOP; cd p5-MOP ; dzil build ; cd MOP-*/ ; cpanm .
One of our 5.10 dependencies is the mro
pragma, but MRO::Compat
gives us compatibility back to 5.6.0.
Perhaps just remove the relevant bits, mostly get_linearized_isa
.
There are a lot of unit tests in the other p5-mop projects that can be imported into this project.
Each test should be converted to match the style of this project (no syntactic sugar used, using Test::More::subtest
, etc) and placed in the appropriate folder.
Projects:
All the previous prototypes had a more specifically designed in instance
type/protocol, we have avoided that so far in this one. This requires some discussion, but we are currently leaning towards not providing anything beyond the base HASH ref instance version, while making sure it is possible to do other reference types if needed.
The idea is that 99% of Perl objects are just HASH refs and people are just fine with that, so while we do need to make it possible to support other ref types, the use of them is an anomaly and so should not slow down or complicate the HASH ref code path.
Relevant Commits
This dist (and related dists) seems to prefer the github issue tracker. If you add the following to the [MetaResources] plugin then metacpan/search.cpan will link to the github issue tracker, and the RT issue tracker will display a banner linking here as well, which could lessen potential confusion.
bugtracker.web = https://github.com/stevan/p5-MOP/issues
There is value in knowing that a method was not defined in a package, meaning that it is not owned by that package. But is there value in knowing this when it comes to required methods?
Required methods will get composed in from roles, and they will be inherited from abstract classes, but they are mostly just markers that something is missing. They can't conflict since their bodies are always the same (NULL) and when you have two of the same name it is the same as having one.
Currently we create a new CV each time (source) so we aren't even aliasing them now, the question is do we even want to.
The key thing I can think of that we are missing is that we don't know the original role/class of the required method, which could be very useful in debugging.
I have no idea what $Foo::
is used for, I know that %Foo::
provides access to the stash but $Foo::
seems to just return undef
without error.
perl -e 'package Foo; package main; use strict; use warnings; use Devel::Peek; Dump($Foo::)'
SV = NULL(0x0) at 0x7f9d13003450
REFCNT = 1
FLAGS = ()
Currently we compose mop::module by hand into mop::role, this works just fine.
Should we keep it this way, or should we look at trying to use the actual role composition mechanism?
Source: https://github.com/stevan/p5-mop/blob/master/mop/lib/mop/role.pm#L20-L44
This is something we fixed with methods but needs to also be done with attributes. When calculating if an attribute belongs to a class/role or not, two things are checked. First, we check to see if the origin_class
of the attribute is the same as us, if not, we then need to check if the attribute was aliased to us from a role or not. We do the first step, but not the second step, this needs fixing and tests.
Note in source: https://github.com/stevan/p5-mop/blob/master/lib/mop/role.pm#L495
As per subject. Grep:
http://search.cpan.org/grep?release=MOP-0.10;string=Devel::Hook;n=1;C=0
Sample fail report:
Currently, via the mop::role
API, we define abstract-ness with two different criteria. First, does a class/role have any required methods? This is the more classic definition of "abstract" in which the class/role is not complete and therefore cannot be instantiated. Second, is the class/role explicitly marked as "abstract"? This is the case where you, the programmer, decided the class was abstract.
By making the is-abstract
check have to look at the set of required methods, we slow down all instance construction. If instead we only looked at the $IS_ABSTRACT
flag/package-variable, and relied on the fact that required methods would be checked at compile time and set the $IS_ABSTRACT
accordingly, then we could speed things up and avoid a lot of runtime meta layer calls.
I also think that this is better left as a choice to the object-system developer, the mop itself should not enforce this.
Notes in the source: https://github.com/stevan/p5-mop/blob/master/mop/lib/mop/role.pm#L88
Probably just remove the word "CLASS" and that is all, but if we can find a better name (see #2) then we can use that.
Source: https://github.com/stevan/p5-mop/blob/master/mop/lib/mop/util.pm#L16-L18
We need to have a DOES
that functions according to the spec in mop::object
.
Sub::Util has been in core since 5.21.4.
I might write a patch for this later.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.