Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can you dynamically generate values for use with traits?

Tags:

oop

traits

raku

For a library I'm writing, I have an attribute on a HOW that uses the handles trait to delegate methods of various roles done by another HOW it uses to an instance of that HOW. My first attempt looked like this (though to make this easier to read, this will only deal with Metamodel::Naming):

class ParentHOW does Metamodel::Naming {
    method new_type(ParentHOW:_: Str:D :$name!, Str:D :$repr = 'P6opaque' --> Mu) {
        my ::?CLASS:D $meta := self.new;
        my Mu         $type := Metamodel::Primitives.create_type: $meta, $repr;
        $meta.set_name: $type, $name;
        $type
    }
}

class ChildHOW {
    has Mu $!parent;
    has Mu $!parent_meta handles <set_name shortname set_shortname>;

    submethod BUILD(ChildHOW:D: Mu :$parent is raw) {
        $!parent      := $parent;
        $!parent_meta := $parent.HOW;
    }

    method new_type(ChildHOW:_: Mu :$parent is raw) {
        my ::?CLASS:D $meta := self.new: :$parent;
        Metamodel::Primitives.create_type: $meta, $parent.REPR
    }

    method name(ChildHOW:D: Mu \C --> Str:_) { ... }
}

my Mu constant Parent = ParentHOW.new_type: :name<Parent>;
my Mu constant Child  = ChildHOW.new_type:  :parent(Parent);

say Child.^shortname; # OUTPUT: Parent

The problem with this is if any of the types I make this HOW handle methods for ever changes, this will no longer work with all of their methods. So instead I want to dynamically generate a list of methods that should be handled given a list of methods that this HOW overrides and a list of types whose methods should be handled. This is not as easy as it sounds because of how the handles trait is implemented. For instance, this won't work:

has Mu $!parent_meta handles do {
    my Array[Str:D] constant PARENT_METHOD_OVERRIDES .= new: <name>;

    ((), Metamodel::Naming)
        .reduce({ (|$^methods, |$^role.HOW.methods: $^role) })
        .map(*.name)
        .grep(PARENT_METHOD_OVERRIDES ∌ *)
};

So how would you dynamically generate a value for a trait to use in cases like this?

like image 202
Kaiepi Avatar asked Nov 02 '19 16:11

Kaiepi


1 Answers

Trait application is set up during compilation, so we need a way to generate a value for it to use during compilation as well. This can be done using the BEGIN phaser, but this is better written with constant in this case.

Constants in Raku are not just variables you can't assign or bind to after declaring them. Normally, when you declare a variable, its symbol is installed during compilation, but its value isn't actually set until runtime, which is why this can happen:

my Int:D $foo = 1;

BEGIN say $foo; # OUTPUT: (Int)

This is not the case with constant; the compiler sets the symbol's value during compilation. This means that for the example in the question, we can dynamically generate a list of methods for handles to use like this:

my Array[Str:D] constant PARENT_METHOD_OVERRIDES .= new: <name>;
my Array[Str:D] constant PARENT_METHODS          .= new:
    ((), Metamodel::Naming)
        .reduce({ (|$^methods, |$^role.HOW.methods: $^role) })
        .map(*.name)
        .grep(PARENT_METHOD_OVERRIDES ∌ *);

has Mu $!parent;
has Mu $!parent_meta handles PARENT_METHODS;

If for some reason symbols like PARENT_METHOD_OVERRIDES and PARENT_METHODS shouldn't exist within the context of the type, you can still handle traits this way by declaring the constants and adding the attributes from within a closure; method and attribute declarations are scoped in such a way that you can write them from anywhere within the type's package and still have them be added to the type. Bear in mind that methods and attributes are handled during compilation, so this is not how you would handle something like dynamically generating attributes or methods for a type.

like image 107
Kaiepi Avatar answered Oct 21 '22 23:10

Kaiepi