Skip to content

Latest commit

 

History

History
366 lines (267 loc) · 13.1 KB

metamodel.pod

File metadata and controls

366 lines (267 loc) · 13.1 KB

The Rakudo Metamodel

Warning

What follows is the current way this works in Rakudo. Parts of it may one day become spec, other bits likely never will. All of it is liable to change as we work out what should be spec and what shouldn't be, and also co-ordinate with other implementations and interested parties to make sure those bits that we determine should be specification are spec'd in a way that matches our shared desires and needs, so far as that's possible. It goes without saying that in doing the things described in this document, you're walking barefoot through a construction site. For now, tread carefully, and be prepared to patch up in response to changes.

Overview

Meta-objects are simply objects that describe parts of the object model. The metamodel lays down the interface that these objects should provide. In Rakudo we have several types of meta-object, all of which have an associated API (sometimes known as a Meta-object Protocol, just to make sure the whole topic appears to be sufficiently scary to outsiders, or something). This document defines the API for:

Packages meta-objects (representing classes, grammars, roles, etc); ones included in Rakudo include ClassHOW, GrammarHOW and RoleHOW.
Attribute meta-objects (representing attributes); the default one of these is simply called Attribute.
Composition meta-objects (e.g. specifying role composition algorithms)

The composition model warrants a little explanation, since it is broken into a couple of parts. We'll stick with classes and roles for now, but we define the interface in the expectation that one day we might want to have things that are composed into a class following some composition algorithm that may be given a different name. Thus we talk in terms of "composables".

There are two important things. First, the actual composer - implementing the composition algorithm - is not a part of the thing we're composing or the thing we're composing into (that is, it is independent of the class and the role). Second, it is up to the thing being composed (e.g. the role in the case of composing a role into a class) to supply the composer.

Package meta-object API (aka HOW API)

This is the API for packages. When we compile something like:

class Town is Place {
    method bar() { say "mmm beer" }
}

Then it results in a set of calls like:

my $temp = ClassHOW.new('Town');
&trait_mod:<is>($temp, Place);
$temp.^add_method('bar', anon method bar() { say "mmm beer" });
::Town := $temp.^compose();

Most of these are calls on the meta-class to methods that give meaning to the keywords the user wrote in their code. The following methods are supported as a minimum.

method new($name?)

Creates something that knows how to provide its metaclass, e.g. through the same mechanism as .HOW obtains it. It need not be the final type-object that may be installed in a namespace or lexpad - that is for compose to return. However, there's nothing to stop it being.

Whether a new instance of the meta-class is created or not is totally up to the implementation of the new method. For the standard Perl 6 class keyword in Rakudo, we create an instance of the meta-class and a temporary object that only knows how to reference the meta-class instance. However, if you were doing a more prototype-OO implementation, then you could instead have the meta-class be a singleton and return a new object, and the object itself knows completely about its methods, attributes and so forth, rather than this knowledge belonging to the meta-class.

add_method($meta, $name, &code_ref)

Adds a method to the methods table of $meta under the given $name and with the given implementation.

add_attribute($meta, $name)

Adds an attribute of the given $name to $meta.

add_parent($meta, $parent)

Adds the given parent to $meta.

add_composable($meta, $composee)

Takes something that we are able to compose (for example, a role) and adds it to the composition list. Certainly, none of the built-in implementations of add_composable immediately perform any composition at this point. Instead, they add the composable to a "to do" list, and at the point we call "compose" to finish the composition of the package, and the application of all the composables takes place. You probably want to do something similar.

applier_for($meta, $target)

For non-composables (that is, packages that cannot be composed into others), this is an error. Otherwise, it returns something that we can use to apply the current package to the target. The thing returned should implement the composer API. It may be convenient to implement this is a set of multi subs.

compose($meta)

Finalizes the creation of the package. It can do any other composition-time operations, such as role composition and calling the composition hook on all attributes added to the package. Returns the type object that we're going to actually install into the namespace or lexical pad, or just return if it's an anonymous declaration.

This is the declarational part of the API, however the introspection part should also be implemented. Please see the Introspection section of S12 for details on this.

Attribute meta-object API

This is the API that objects representing attributes should expose. The attribute meta-object is responsible for generating any accessor and/or delegation methods associated with the attribute.

new($name, :$has-accessor, :$rw, :$handles, :$build, :$type)

Creates a new attribute meta-object, initialized with the name, whether or not the attribute has an accessor, whether or not that accessor 'is rw' and - if there was a handles trait modifier on the attribute - the handles trait modifier.

compose($meta-package)

Takes the attribute and does any final composition tasks (such as installing any accessor methods and/or delegators). The parameter is the meta-object of the package that the attribute belongs to; you can call .add_method on it to add methods, for example.

Composer meta-object API

The composer is responsible for composing something composable (in standard Perl 6, that's a role) into some other object (perhaps a class or another role or an instance). The minimal interface need only support one method, but it may well be that a composee and a composer choose to share knowledge of more than this (for example, a "requires" or "conflicts" list).

apply($target, @composees)

Applies all of the composees to the target, or throws an exception if there is a problem with doing so. It's totally up to the composer exactly what it does; the default composer for Perl 6 roles will construct a single intermediate role and then compose that into the target, for example. Since the model is intended for more general composition-y things rather than just roles as are commonly defined today, we choose to give the composer a view of all of the composees.

Metaclass Compatibility

Warning: Conjectural.

One rather complex issue we run into is what happens if you want to inherit from something that has a different metaclass. For now, we require that if some class S isa T, then also S.HOW isa T.HOW. This means that all other types of class-ish things that want to have a custom metaclass should subclass ClassHOW (either directly or transitively). Thus, this is fine:

class A { }
thingy B is A { }
otherthingy C is B { }

If the following is true:

OtherThingyHOW.isa(ThingyHOW) and ThingyHOW.isa(ClassHOW)

Composer Compatibility

Warning: Conjectural.

TODO: Provide a solution to the problems described here.

Given it's the things we compose that determine what composer to use, we may easily run into a situation where different things want a different composer. At some level that's OK - if we want to support a more general notion of "things that do something composition-ish" then it is probably too restrictive to just always make this an error in the long run. For now, however, we do just that; when we have a good solution, we can relax the restriction.

We do have the nicety that once we hit runtime, since composition is flattening by nature, we don't have any relationship at runtime with something that was composed in (besides keeping it in our list of "things that we composed"). Thus the problem of the behaviors of two different appliers is only a composition-time issue and not a runtime one.

Associating a package declarator with a metaclass

Rakudo provides two levels of hookage for creating new types of package declarator. You will very likely only need this one, which is the HOW map, %*HOW. This is simply a hash that maps the name of a scope declarator to the name of the HOW to create. At the entry point to your derived grammar, you should temporize the current HOW hash from the calling language, and add mappings from names of package declarators that you will introduce to the HOW to use. By default, this hash contains things like:

{ class => 'ClassHOW', role => 'RoleHOW' }

It's completely fine for multiple package declarators to map to the same HOW - you may just wish to introduce a new one as better documentation but not need to do anything more special in terms of the meta-model. Note that your rule for parsing the scope declarator sets the name of the thing in this map in the $*PKGDECL global. For example, here is one from STD.

token package_declarator:role {
    :my $*PKGDECL ::= 'role';
    <sym> <package_def>
}

You should do the same (and it's probably nice if what you set matches the name of the symbol you parse).

Meta-programming Example: Adding AOP Support

Note that this currently does not work in Rakudo, and will probably change a bit. It's purpose is mostly a thought experiment to try and help sanify the design of the metamodel.

slang AOP {
    method TOP {
        temp %*HOW;
        %*HOW<aspect> := AspectHOW;
        my $lang = self.cursor_fresh( AOP );
        $lang.comp_unit;
    }
    token package_declarator:sym<aspect> {
        :my $*PKGDECL := 'aspect';
        <sym> <package_def>
    }
}

class AspectComposer {
    method apply($target, @aspects) {
        my @wrappables = $target.methods(:local);
        for @aspects -> $aspect {
            for $aspect.method_wrappers.kv -> $name, $wrapper {
                my ($wrappee) = @wrappables.grep({ .name eq $name });
                if $wrappee {
                    $wrappee.wrap($wrapper);
                }
                else {
                    die "No sub found to wrap named '$name'";
                }
            }
        }
    }
}

class Aspect {
    has $.HOW;
    has $.Str;
    method WHAT() { self }
    method defined() { False }
}

class AspectHOW {
    has %.method_wrappers;

    method new($name) {
        return Aspect.new(
            HOW => self.bless(*),
            Str => $name ~ "()"
        );
    }
    
    method add_method($obj, $name, $method) {
        $.method_wrappers{$name} = $method;
    }
    
    multi method composer_for($obj, $ where { .can('methods') }) {
        return AspectComposer;
    }
    
    multi method composer_for($obj, Object) {
        die "Can only apply aspects to things that expose methods";
    }

    method compose($obj) {
        return $obj;
    }

    method add_attribute($meta, $attr) {
        die "Aspects do not support attributes";
    }

    method add_parent($meta, $parent) {
        die "Aspects do not support inheritance";
    }

    method add_composable($meta, $composable) {
        die "Aspects do not support being composed into";
    }
}

This could then we used as something like:

use AOP;

aspect LogToStderrToo {
    method log($message) {
        $*ERR.say($message);
        nextsame;
    }
}

class ErrorLog does LogToStderrToo {
    method log($message) {
        my $fh = open("log", :a);
        $fh.say($message);
        $fh.close;
    }
}

Note that a usable implementation would want a bit more than this, and have many other design considerations.

Influencing package code generation

Note: This is highly Rakudo-specific and very likely to remain that way. The good news is that you won't need to do it often.

Rakudo has a compile-time representation of the package currently being compiled. This is the thing that ends up actually generating the code - PAST nodes - that make the calls on the metaclass. By default, we always create an instance of Perl6::Compiler::Package, apart from for roles and modules, for which we need to do some slightly different code generation - those use a subclass of it, such as Perl6::Compiler::Role. You may modify %*PKGCOMPILER, again keying on $*PKGDECL, to specify something other than the default. You should then write a subclass of an existing handler for this and implement the same interface (plus any other bits you'll need - it's just a class).