rakulang, dartlang, nimlang, golang, rustlang, lang lang no see

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 成为一个好的编程语言

原文链接: https://gfldex.wordpress.com/2021/02/17/method-ish/