我正在继续探索Perl6深层次的细微实现细节。这次我将自己的方法安装到角色中时遇到问题。当我们开始进入代码之旅时,请系好安全带。
这个想法是一个属性特征,它将方法安装在它所组成的类型对象上。这个问题最初是在私有方法上发现的,我希望将其安装在声明属性的角色中。此时我发现在某些条件下生成的方法引用了来自其闭包的标量无法调用!很可能是由于关闭在运行时丢失。但最令人困惑的一点是,它只发生在角色上,只有当一个角色正在消耗另一个角色时!
所以,这是特质来源:
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;
}
}
}
这里的关键点是install-method
,它安装了一个公共方法pubattr-<attr>
,以及私有方法attr-<attr>
,check-<attr>
。 pubattr-
,attr-
和check-
之间的区别在于前两个是指他们的封闭,而后者则没有。如果在各个文件中定义了两个角色和一个类,会发生以下情况:
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;
由-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;
}
}
由-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;
}
}
执行compose_method_inject.p6会产生以下输出:
> 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)
请注意,该类工作正常,而BarRole
中的类似代码失败。如果首先执行来自foo-role-test
的FooRole
,则会观察到相同的结果:
> 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)
值得注意的是,从FooRoleHOW
安装的方法不会失去其关闭并且已成功执行。
现在,另一招。我从does BarRole
中删除FooRole
并将其直接应用于Foo:
class Foo does FooRole does BarRole {
输出变化很大,情况变得更加混乱:
> 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另一个需要注意的重要事项是,角色和类有意地按文件分割,因为将它们全部放在公共文件中会使事情按预期工作。
顺便说一下,我不想深入了解它,但在原始代码中,从上面提取上述样本我也用.set_name
设置方法名称。名称是字符串,包括封闭的$attr
标量。 compose()
中的转储方法表生成以集合名称为值的哈希值;在用户代码中转储相同的表显示类似于上面的输出 - 使用<anon>
作为值。看起来,方法名称与封闭一起进行了GC。
现在,我想听一些人说我很蠢,方法必须以不同的方式安装。或者有关属性的信息必须以其他方式保留,而不是依赖于闭包。或任何其他想法,让我创建私有属性相关的方法。
这不是答案,而是一个注释和bug的解决方法。好的,这个说明刚刚完成:这是一个错误。虽然在rakudo的Linux版本中不存在,但我只在macOS / darwin上观察它。当然,这并不意味着其他平台不易受攻击。
该bug有一个解决方法。由于从类/角色组合器安装的方法不会丢失它们的闭包,因此必须将方法的安装移入它们中。在我的情况下,因为两者都需要类似的功能,所以使用角色实现方法安装器作为魅力。