Metaobject protocol (MOP)
Introspection and the Raku object system
1 | Metamethods |
1.1 | WHAT |
1.2 | WHICH |
1.3 | WHO |
1.4 | WHERE |
1.5 | HOW |
1.6 | WHY |
1.7 | DEFINITE |
1.8 | VAR |
2 | Structure of the metaobject system |
2.1 | Bootstrapping concerns |
2.2 | Composition time and static reasoning |
2.3 | Power and responsibility |
2.4 | Power, convenience and pitfalls |
Raku is built on a metaobject layer. That means that there are objects (the metaobjects) that control how various object-oriented constructs (such as classes, roles, methods, attributes or enums) behave.
The metaobject has a practical benefit to the user when a normal object's type is needed. For example:
my = [1, 2];sub show-type()show-type ; # OUTPUT: «Array»
To get a more in-depth understanding of the metaobject for a class
, here is an example repeated twice: once as normal declarations in Raku, and once expressed through the metamodel:
A.x();
corresponds to:
constant A := Metamodel::ClassHOW.new_type( name => 'A' ); # class A {A.^add_method('x', my method x(A:) ); # method x()A.^compose; # }A.x();
(except that the declarative form is executed at compile time, and the latter form does not).
The metaobject behind an object can be obtained with $obj.HOW
, where HOW stands for Higher Order Workings (or, HOW the *%@$ does this work?).
Here, the calls with .^
are calls to the metaobject, so A.^compose
is a shortcut for A.HOW.compose(A)
. The invocant is passed in the parameter list as well, to make it possible to support prototype-style type systems, where there is just one metaobject (and not one metaobject per type, as standard Raku does it).
As the example above demonstrates, all object oriented features are available to the user, not just to the compiler. In fact the compiler just uses such calls to metaobjects.
Metamethods
These are introspective macros that resemble method calls.
Metamethods are generally named with ALLCAPS, and it is considered good style to avoid creating your own methods with ALLCAPS names. This will avoid conflicts with any metamethods that may appear in future versions of the language.
WHAT
The type object of the type. This is a pseudo-method that can be overloaded without producing error or warning, but will be ignored.
For example 42.WHAT
returns the Int
type object.
WHICH
The object's identity value. This can be used for hashing and identity comparison, and is how the ===
infix operator is implemented.
WHO
The package supporting the object.
WHERE
The memory address of the object. Note that this is not stable in implementations with moving/compacting garbage collectors. Use WHICH
for a stable identity indicator.
HOW
Returns the metaclass object, as in "Higher Order Workings".
say (%).HOW.^name # OUTPUT: «Perl6::Metamodel::ClassHOW+{<anon>}»
HOW
returns an object of type Perl6::Metamodel::ClassHOW
in this case; objects of this type are used to build classes. The same operation on the &
sigil will return Perl6::Metamodel::ParametricRoleGroupHOW
. You will be calling this object whenever you use the ^
syntax to access metamethods. In fact, the code above is equivalent to say (&).HOW.HOW.name(&)
which is much more unwieldy. Metamodel::ClassHOW is part of the Rakudo implementation, so use with caution.
WHY
The attached Pod value.
DEFINITE
The object has a valid concrete representation. This is a pseudo-method that can be overloaded without producing error or warning, but will be ignored.
Returns True
for instances and False
for type objects.
VAR
Returns the underlying Scalar
object, if there is one.
The presence of a Scalar
object indicates that the object is "itemized".
.say for (1, 2, 3); # OUTPUT: «123», not itemized.say for $(1, 2, 3); # OUTPUT: «(1 2 3)», itemizedsay (1, 2, 3).VAR ~~ Scalar; # OUTPUT: «False»say $(1, 2, 3).VAR ~~ Scalar; # OUTPUT: «True»
Structure of the metaobject system
Note: this documentation largely reflects the metaobject system as implemented by the Rakudo Raku compiler, since the design documents are very light on details.
For each type declarator keyword, such as class
, role
, enum
, module
, package
, grammar
or subset
, there is a separate metaclass in the Metamodel::
namespace. (Rakudo implements them in the Perl6::Metamodel::
namespace, and then maps Perl6::Metamodel
to Metamodel
).
Many of these metaclasses share common functionality. For example roles, grammars and classes can all contain methods and attributes, as well as being able to do roles. This shared functionality is implemented in roles which are composed into the appropriate metaclasses. For example role Metamodel::RoleContainer implements the functionality that a type can hold roles and Metamodel::ClassHOW, which is the metaclass behind the class
keyword, does this role.
Most metaclasses have a compose
method that you must call when you're done creating or modifying a metaobject. It creates method caches, validates things and so on, and weird behavior ensues if you forget to call it, so don't :-).
Bootstrapping concerns
You might wonder how Metamodel::ClassHOW
can be a class, when being a class is defined in terms of Metamodel::ClassHOW
, or how the roles responsible for role handling can be roles. The answer is by magic.
Just kidding. Bootstrapping is implementation specific. Rakudo does it by using the object system of the language in which itself is implemented, which happens to be (nearly) a subset of Raku known as NQP. NQP has a primitive, class-like kind called knowhow
, which is used to bootstrap its own classes and roles implementation. knowhow
is built on primitives that the virtual machine under NQP provides.
Since the object model is bootstrapped in terms of lower-level types, introspection can sometimes return low-level types instead of the ones you expect, like an NQP-level routine instead of a normal Routine object, or a bootstrap-attribute instead of Attribute.
Composition time and static reasoning
In Raku, a type is constructed as it is parsed, so in the beginning, it must be mutable. However if all types were always mutable, all reasoning about them would get invalidated at any modification of a type. For example the list of parent types and thus the result of type checking can change during that time.
So to get the best of both worlds, there is a time when a type transitions from mutable to immutable. This is called composition, and for syntactically declared types, it happens when the type declaration is fully parsed (so usually when the closing curly brace is parsed).
If you create types through the metaobject system directly, you must call .^compose
on them before they become fully functional.
Most metaclasses also use composition time to calculate some properties like the method resolution order, publish a method cache, and other house-keeping tasks. Meddling with types after they have been composed is sometimes possible, but usually a recipe for disaster. Don't do it.
Power and responsibility
The metaobject protocol offers much power that regular Raku code intentionally limits, such as calling private methods on classes that don't trust you, peeking into private attributes, and other things that usually simply aren't done.
Regular Raku code has many safety checks in place; not so the metamodel. It is close to the underlying virtual machine, and violating the contracts with the VM can lead to all sorts of strange behaviors that, in normal code, would obviously be bugs.
So be extra careful and thoughtful when writing metatypes.
Power, convenience and pitfalls
The metaobject protocol is designed to be powerful enough to implement the Raku object system. This power occasionally comes at the cost of convenience.
For example, when you write my $x = 42
and then proceed to call methods on $x
, most of these methods end up acting on the integer 42, not on the scalar container in which it is stored. This is a piece of convenience found in ordinary Raku. Many parts of the metaobject protocol cannot afford to offer the convenience of automatically ignoring scalar containers, because they are used to implement those scalar containers as well. So if you write my $t = MyType; ... ; $t.^compose
you are composing the Scalar that the $
-sigiled variable implies, not MyType
.
The consequence is that you need to have a rather detailed understanding of the subtleties of Raku in order to avoid pitfalls when working with the MOP, and can't expect the same "do what I mean" convenience that ordinary Raku code offers.