Method-ish
— 焉知非鱼Method-ish
在我的上一篇文章中,我又一次为从 CORE 中增强类的方法而苦恼。这种挣扎完全没有必要,因为我并没有用增加的方法改变对象的状态。对于做更高级的东西,我可能不得不这样做。把手伸进 Raku 的内部这么深,我可能会把自己烫伤。既然我想做的是把我的代码绑在编译器的变化上,反正我可能会全身心地投入到 nqp-land 中去。
my \j = 1 | 2 | 3;
dd j;
use nqp;
.say for nqp::getattr(j, Junction, '$!eigenstates');
# OUTPUT: any(1, 2, 3)
1
2
3
我们可以使用 nqp 来获取私有属性,而不需要添加任何方法。这就有点儿不伦不类了。所以,让我们用一个伪方法来做一些 deboilerplating。
sub pry(Mu $the-object is raw) {
use InterceptAllMethods;
class Interceptor {
has Mu $!the-object;
method ^find_method(Mu \type, Str $name) {
my method (Mu \SELF:) is raw {
use nqp;
my $the-object := nqp::getattr(SELF, Interceptor, '$!the-object');
nqp::getattr($the-object, $the-object.WHAT, '$!' ~ $name)
}
}
}
use nqp;
nqp::p6bindattrinvres(nqp::create(Interceptor), Interceptor, '$!the-object', $the-object);
}
.say for j.&pry.eigenstates;
# OUTPUT: 1
2
3
通过 InterceptAllMethods,lizmat 改变了类关键字的行为,允许我们提供一个 FALLBACK-method 来捕获任何方法,包括从 Mu 继承的方法。这反过来又允许 pry 返回的对象将任何方法调用转移到一个自定义的方法。在这个方法中,我们可以对 .&pry
被调用的对象做任何我们想做的事情。
由于我们的特殊对象会拦截任何调用,甚至是 Mu 的调用,我们需要找到另一种方法来调用 .new
。由于 .^
不是 .
的特殊形式,我们可以用它来获得对类方法的访问。
sub interceptor(Method $the-method){
use InterceptAllMethods;
use nqp;
sub (Mu $the-object is raw) {
my class Interceptor {
has Mu $!the-object;
has Code $!the-method;
method ^find_method(Mu \type, Mu:D $name) {
my method (Mu \SELF: |c) is raw {
$!the-method.($!the-object, $name, |c)
}
}
method ^introspect(Mu \type, Mu \obj) {
my method call-it() is raw {
$!the-object
}
obj.&call-it;
}
method ^new(Mu \type, $the-object!, $the-method) {
nqp::p6bindattrinvres(
nqp::p6bindattrinvres(nqp::create(Interceptor), Interceptor, '$!the-object', $the-object),
Interceptor, '$!the-method', $the-method)
}
}
# nqp::p6bindattrinvres(
# nqp::p6bindattrinvres(nqp::create(Interceptor), Interceptor, '$!the-object', $the-object),
# Interceptor, '$!the-method', $the-method);
Interceptor.^new($the-object, $the-method)
}
}
my &first-defined = interceptor(
my method (Positional \SELF: $name) {
for SELF.flat -> $e {
with $e."$name"(|%_) {
.return
}
}
Nil
}
);
my $file = <file1.txt file2.txt file3.txt nohup.out>».IO.&first-defined.open(:r);
dd $file;
# OUTPUT: Handle $file = IO::Handle.new(path => IO::Path.new("nohup.out", :SPEC(IO::Spec::Unix), :CWD("/home/dex/projects/raku/tmp")), chomp => Bool::True, nl-in => $["\n", "\r\n"], nl-out => "\n", encoding => "utf8")
sub interceptor
接受一个方法并返回一个 sub。如果这个 sub 像方法一样被调用,它将把要被调用的方法的名称和调用者转发给一个自定义方法。当 .&first-defined
被调用时,会返回一个特殊的对象。让我们来看看它是什么。
my \uhhh-special = <a b c>.&first-defined;
dd uhhh-special.^introspect(uhhh-special);
# OUTPUT: ($("a", "b", "c"), method <anon> (Positional \SELF: $name, *%_) { #`(Method|93927752146784) ... })
我们必须给 .^introspect
一个我们想看的对象,因为它的调用者是类 Interceptor 的类型对象。
目前,我还不知道有什么办法(毕竟,我知道的只是足够多的东西,真的很危险。这是不幸的,因为 lizmat 决定重载关键字 class
,而不是用不同的名字导出特殊的 Metamodel::ClassHOW
。如果我们不想或不能有外部依赖,我们可以使用 MOP 来创建我们的类型对象。
class InterceptHOW is Metamodel::ClassHOW {
method publish_method_cache(|) { }
}
sub ipry(Mu $the-object is raw) {
my \Interceptor = InterceptHOW.new_type(:name<Interceptor>);
Interceptor.^add_attribute(Attribute.new(:name<$!the-object>, :type(Mu), :package(Interceptor)));
Interceptor.^add_meta_method('find_method',
my method find_method(Mu \type, Str $name) {
# say „looking for $name“;
my method (Mu \SELF:) is raw {
use nqp;
my $the-object := nqp::getattr(SELF, Interceptor, '$!the-object');
nqp::getattr($the-object, $the-object.WHAT, '$!' ~ $name)
}
});
Interceptor.^compose;
use nqp;
nqp::p6bindattrinvres(nqp::create(Interceptor), Interceptor, '$!the-object', $the-object);
}
当我写这篇文章的时候,我发现 .^add_meta_method
只有在提供给它的方法的第一个参数的名字和 Str
相同的时候才会工作。起初,我尝试了一个匿名方法,它最终出现在 .^meta_method_table
中,但从未被调用。我想这个 bug 其实并不重要,因为这个元方法根本没有被记录下来。如果我玩火,我没有权利抱怨烧伤。你会在野外的 Actions.nqp
中发现这个方法。Class 关键字并没有什么神奇的作用。Rakudo 只是使用 MOP 来构造类型对象。
我们不能在 Raku 中重载赋值操作符。这其实并不需要,因为赋值是通过调用一个名为 STORE 的方法来实现的。由于我们得到了对 dispatch 的完全控制,我们可以拦截任何方法调用,包括一连串的方法调用。
multi sub methodify(%h, :$deeply!) {
sub interceptor(%h, $parent = Nil){
use InterceptAllMethods;
use nqp;
class Interceptor is Callable {
has Mu $!the-object;
has Mu @!stack;
method ^find_method(Mu \type, Mu:D $name) {
my method (Mu \SELF: |c) is raw {
my @new-stack = @!stack;
my $the-object = $!the-object;
if $name eq 'STORE' {
# workaround for rakudobug#4203
$the-object{||@new-stack.head(*-1)}:delete if $the-object{||@new-stack.head(*-1)}:exists;
$the-object{||@new-stack} = c;
return-rw c
} else {
@new-stack.push: $name;
my \nextlevel = SELF.^new($!the-object, @new-stack, $name);
nextlevel
}
}
}
method ^introspect(Mu \type, Mu \obj) {
my method call-it() is raw {
$!the-object, @!stack
}
obj.&call-it;
}
method ^new(Mu \type, $the-object!, @new-stack?, $name?) {
$name
?? nqp::p6bindattrinvres(
nqp::p6bindattrinvres(nqp::create(Interceptor), Interceptor, '$!the-object', $the-object),
Interceptor, '@!stack', @new-stack)
!! nqp::p6bindattrinvres(nqp::create(Interceptor), Interceptor, '$!the-object', $the-object)
}
}
Interceptor.^new(%h)
}
interceptor(%h)
}
my %h2;
my $o2 = methodify(%h2, :deeply);
$o2.a.b = 42;
dd %h2;
$o2.a.b.c = <answer>;
dd %h2;
say $o2.a.b.c;
# OUTPUT: Hash %h2 = {:a(${:b(\(42))})}
Hash %h2 = {:a(${:b(${:c(\("answer"))})})}
This type cannot unbox to a native string: P6opaque, Interceptor
in block <unit> at /home/dex/projects/raku/any-chain.raku line 310
每当我们调用一个方法时,都会创建一个新的 Interceptor 实例,它存储了前一个方法的名称。这样我们就可以沿着方法调用链移动。由于赋值调用 STORE,我们可以将赋值转移到我们用作实际数据结构的 Hash 中。唉,检索值就不一样了,因为 Raku 不区分方法调用和 FETCH。在这里,龙比我强。我还是包含了这个一半失败的尝试,因为我对 slippy 半列表有很好的利用。这需要使用 v6.e.preview
,让我踩到了一个 bug。可能还有更多这样的情况。所以请使用同样的,这样我们就可以在 .e
发布到野外之前,把所有的野兽都杀掉。
能够完全控制方法调用链将是一件好事。也许我们可以用 RakuAST 来做到这一点。
有了这些已经可以工作的东西,我们可以做一些有趣的事情。那些烦人的异常总是在拖我们的后腿。我们可以用 try
来化解它们,但那会破坏一个方法调用链。
constant no-argument-given = Mu.new;
sub try(Mu $obj is raw, Mu $alternate-value = no-argument-given) {
interceptor(my method (Mu \SELF: $name, |c) {
my $o = SELF;
my \m = $o.^lookup($name) orelse {
my $bt = Backtrace.new;
my $idx = $bt.next-interesting-index($bt.next-interesting-index + 1);
(X::Method::NotFound.new(:method($name), :typename($o.^name)) but role :: { method vault-backtrace { False }}).throw(Backtrace.new($idx + 1));
}
try {
$o = $o."$name"(|c);
}
$! ~~ Exception
?? $alternate-value.WHICH eqv no-argument-given.WHICH
?? $o
!! $alternate-value
!! $o
}).($obj)
}
class C {
has $.greeting;
method might-throw { die "Not today love!" }
method greet { say $.greeting }
}
C.new(greeting => ‚Let's make love!‘).&try.might-throw.greet;
# OUTPUT: Let's make love!
伪方法 try 将会化解任何异常,并允许继续调用 C 语言的方法。我必须用一个特殊的值来标记没有可选的参数 $alternate-value
,因为它实际上可能会把异常对象变成 Nil
。
我很肯定还有很多这样的小帮手在等着我们去发现。未来可能会有一个模块,希望能帮助 Raku 成为一个好的编程语言。