Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Traits, attributes, roles and closures

Tags:

raku

I'm continuing my quest into the deep waters of Perl6 subtle implementation details. This time I have a problem with installing own methods into a role. Fasten your seatbelts please as we start the journey into the code.

The idea is an attribute trait which installs methods on type objects it's being composed into. The problem was initially discovered on private methods which I would expect to be installed in the role the attribute is being declared in. At that point I have discovered that under certain conditions generated methods referring a scalar from their closure cannot be called! Most likely due to the closure being lost at run-time. But the most confusing point is that it happens only to the roles and only if one role is consuming another!

So, here is the trait source:

 unit module trait-foo;

 role FooClassHOW {...}

 role FooAttr {
     has $.base-name = self.name.substr(2);
     method compose (Mu \type) {
         callsame;
         if (type.HOW ~~ Metamodel::ClassHOW) && (type.HOW !~~ FooClassHOW) {
             type.HOW does FooClassHOW;
         }
     }

     method install-method ( Mu \type ) {
         my $attr = self;
         type.^add_private_method( 
             "attr-{$attr.base-name}", 
             method { "by attr {$attr.name}" } 
         );
         type.^add_method( 
             "pubattr-{$attr.base-name}", 
             method { "by attr {$attr.name} - public" } 
         );
         type.^add_private_method( 
             "check-{$attr.base-name}", 
             method { "not using closure" } 
         );
     }
 }

 role FooClassHOW {
     method compose ( Mu \type ) {
         for type.^attributes.grep( FooAttr ) -> $attr {
             $attr.install-method( type );
             type.^add_private_method( 
                 "class-{$attr.base-name}", 
                 method { "by class: attr {$attr.name}" } 
             );
         }
         nextsame;
     }
 }

 role FooRoleHOW {
     method compose ( Mu \type ) {
         for type.^attributes.grep( FooAttr ) -> $attr {
             $attr.install-method( type );
             type.^add_private_method( 
                 "role-{$attr.base-name}", 
                 method { "by role: attr {$attr.name}" } 
             );
         }
         nextsame;
     }
 }

 multi trait_mod:<is> (Attribute:D $attr, :$foo!) is export {
     $attr does FooAttr;
     given $*PACKAGE.HOW {
         when Metamodel::ParametricRoleHOW {
             $_ does FooRoleHOW unless $_ ~~ FooRoleHOW;
         }
         default {
             $_ does FooClassHOW unless $_ ~~ FooClassHOW;
         }
     }
 }

The key point here is the install-method which installs a public method pubattr-<attr>, and private methods attr-<attr>, check-<attr>. The difference between pubattr-, attr- and check- is that the first two are referring their closure while the latter doesn't. Here is what happens if two roles and a class are defined in their individual files:

compose_method_inject.p6

 #!/usr/bin/env perl6
 use lib '.';
 use trait-foo;
 use compose-foorole;

 class Foo does FooRole {
     has $.fubar is foo;

     method class-test {
         say self!check-fubar;
         say self!class-fubar;
         say self!attr-fubar;
     }
 }

 my $inst = Foo.new;
 note "> Class";
 $inst.class-test;
 note "> BarRole";
 $inst.bar-role-test;
 note "> FooRole";
 $inst.foo-role-test;

compose-foorole.pm6

 unit package compose;
 use trait-foo;
 use compose-barrole;

 role FooRole does BarRole is export {
     has $.foo is foo;

     method foo-role-test {
         note FooRole.^candidates[0].^private_method_table;
         say self!check-foo;
         say self!role-foo;
         say self!attr-foo;
     }
 }

compose-barrole.pm6

unit package compose;
 use trait-foo;

 role BarRole is export {
     has $.bar is foo;

     method bar-role-test {
         note BarRole.^candidates[0].^private_method_table;
         say self!check-bar;
         say self!role-bar;
         say self!inattr-bar;
     }
 }

Executing compose_method_inject.p6 produces the following output:

> Class
not using closure
by class: attr $!fubar
by attr $!fubar
by attr $!fubar - public
> BarRole
{attr-bar => <anon>, check-bar => <anon>, role-bar => <anon>}
not using closure
by role: attr $!bar
Cannot invoke this object (REPR: Null; VMNull)

Note that the class works ok while similar code in BarRole fails. Same result would be observed if foo-role-test from FooRole is executed first:

> Class
not using closure
by class: attr $!fubar
by attr $!fubar
by attr $!fubar - public
> FooRole
{attr-foo => <anon>, check-foo => <anon>, role-foo => <anon>}
not using closure
by role: attr $!foo
Cannot invoke this object (REPR: Null; VMNull)

It is also noteworthy that method installed from FooRoleHOW doesn't lose its closure and is been successfully executed.

Now, to another trick. I remove does BarRole from FooRole and make it applied directly on Foo:

class Foo does FooRole does BarRole {

The output changes drastically and situation becomes even more confusing:

> Class
not using closure
by class: attr $!fubar
by attr $!fubar
by attr $!fubar - public
> FooRole
{attr-foo => <anon>, check-foo => <anon>, role-foo => <anon>}
not using closure
by role: attr $!foo
by attr $!foo
> BarRole
{attr-bar => <anon>, check-bar => <anon>, role-bar => <anon>}
not using closure
by role: attr $!bar
Cannot invoke this object (REPR: Null; VMNull)

UPD Another important thing to note is that both roles and the class a intentionally split by files because placing them all in common file makes things work as intended.

BTW, I don't want get deeper into it, but in the original code where the above samples were extracted from I was also setting methods names with .set_name. Names were strings including the $attr scalar from the closure. Dumping method tables in the compose() was producing hashes with the set names as values; dumping same tables in user code shows output similar to the above – with <anon> as values. Seemingly, method names were GC'ed together with the closure.

Now, I would like to hear from someone that I'm stupid and methods has to be installed differently. Or that the information about attribute has to be preserved in some other way but not through relying on closures. Or any other idea which would let me create private attribute-related methods.

like image 715
Vadim Belman Avatar asked Sep 07 '18 17:09

Vadim Belman


1 Answers

This is not exactly answer but rather a note and a workaround for the bug. Ok, the note has been just made: this is a bug. Though it is not present in Linux version of rakudo, I only observe it on macOS/darwin. Of course, it doesn't mean that other platforms are not vulnerable.

The bug has a workaround. Since methods installed from class/role composers do not lost their closures, then installation of methods has to be moved into them. In my case, because similar functionality is desirable for both of them, use of a role implementing method installator works as a charm.

like image 119
Vadim Belman Avatar answered Sep 28 '22 12:09

Vadim Belman