Wait the light to fall

真正的多态对象

焉知非鱼

true polymorphic objects

关于多态性 #

RFC159 引入了真正的多态对象的概念。

对象可以按需变形为数字、字符串、布尔值和更多其它的东西。因此,对象可以自由地传递和操作,而不必关心它们包含的内容(甚至不必关心它们是对象)。

当我们看看 42"foo"now 在 Raku 中是如何工作的,我们只能看到这种设想已经实现的差不多了。因为大多数时候,人们并不关心 42 真的是一个 Int 对象,“foo” 真的是一个 Str 对象,而每次调用 now 的时候,它都代表一个新的 Instant 对象。人们唯一关心的,是它们可以在表达式中使用。

say "foo" ~ "bar";  # foobar
say 42 + 666;       # 708
say now - INIT now; # 0.0005243

RFC159 列出了一些方法名称,用于指示对象在某些情况下应该如何行事,如果对象的类没有提供该方法,则系统会提供一个回退方法。在大多数情况下,这些方法没有进入 Raku,但其中一些方法用不同的名字进入了 Raku。

Name in RFC Name in Raku When
STRING Str 在字符串上下文中调用
NUMBER Numeric 在数值上下文中调用
BOOLEAN Bool 在布尔上下文中调用

而他们中的一些甚至还保留了自己的名字:

Name in RFC When
BUILD 在对象祝福中调用
STORE 在 lvalue = 上下文中调用
FETCH 在 rvalue = 上下文中调用
DESTROY 在对象销毁时调用

但有时与 RFC 的语义有微妙的不同。

只有少数几条被列入 #

最后,只为 Raku 决定了一组有限的特殊方法。RFC159 中的所有其他方法都是由多态操作符实现的,这些操作符在需要时进行强制。例如,所提出的 PLUS 方法已经被实现为一个 infix + 运算符,它有一个"默认"的候选者,将其操作数强转为一个数字。

因此,实际上,如果你有一个 Foo 类的对象,并且你想让它作为一个数字,你只需要为这个类添加一个 Numeric 方法。一个表达式,如:

my $foo = Foo.new;
say $foo + 42;

正是有效地执行:

say infix:<+>( $foo, 42 );

infix:<+> 候选者,接受 Any 对象,这样做:

return infix:<+>( $foo.Numeric, 42.Numeric );

而如果这样的类 Foo 没有提供 Numeric 方法,那么它就会抛出一个异常。

DESTROY 方法 #

在 Raku 中,对象的销毁是非决定性的。如果一个对象不再使用,它可能会被垃圾收集。可能的部分是因为 Raku 不知道全局销毁阶段,不像 Perl。所以当一个程序完成后,它只是做了一个退出(虽然这个逻辑会尊重任何 END 块)。

当一个对象不能再被"访问"时,它被标记为"准备移除"。然后,当垃圾收集逻辑启动时,它的 DESTROY 方法就会被调用。这可以是在它变得无法到达后的任何时间量。

如果你需要 DESTROY 方法的确定性调用,你可以使用 LEAVE phaser。或者,如果这不能让你搔到你的痒处,你可以可能使用 FINALIZER 模块。

对标量值进行 STORE/FETCH 处理 #

从概念上讲,你可以把 Raku 中的容器看作是一个有 STOREFETCH 方法的对象。每当你在容器中设置一个值的时候,它在概念上就会调用 STORE 方法。而每当需要容器内的值时,它在概念上就会调用 FETCH 方法。在伪代码中:

my $foo = 42;  # Scalar.new(:name<$foo>).STORE(42)

但是如果你想控制对一个标量值的访问,类似于 Perl 的 tie 呢?好吧,在 Raku 中,你可以使用一种特殊类型的容器类,叫做 Proxy。它的一个使用实例如下:

sub proxier($value? is copy) {
    return-rw Proxy.new(
        FETCH => method { $value },
        STORE => method ($new) {
            say "storing";
            $value = $new
        }
    )
}

my $a := proxier(42);
say $a;    # 42
$a = 666;  # storing
say $a;    # 666

子程序在默认情况下返回其结果值时是去容器化的。基本上有两种方法可以确保实际的容器被返回:使用 return-rw(像本例中那样),或者用 is rw trait 标记子程序。

对复合值进行 STORE #

由于 FETCH 只有在标量值上才有意义,所以在 Raku 中不支持 FETCH 在复合值上的应用,比如哈希和数组。我想可以考虑在这种情况下调用 FETCH禅切,但我们决定那只是返回复合值本身。

然而,复合值上的 STORE 方法可以实现一些有趣的功能。每当有整个复合值的初始化时,就会调用 STORE 方法。比如说:

@a = 1,2,3;

基本上是这样执行的:

@a := @a.STORE( (1,2,3) );

但是如果你还没有初始化的 @a 怎么办?那么 STORE 方法实际上应该是创建一个新的对象,并用给定的值来初始化这个对象。而 STORE 方法可以知道,因为这样它也会收到一个名为 INITIALIZE 的参数,其值为 True。所以当你写下这句话的时候:

my @b = 1,2,3;

基本上被执行的是:

@b := Array.new.STORE( (1,2,3), :INITIALIZE );

现在,如果你意识到这一点:

my @b;

实际上是下面这种写法的简写:

my @b is Array;

这只是一小步,你可以意识到,你可以创建自己的类,定制数组逻辑,可以用自己的数组逻辑代替标准的数组逻辑。观察一下:

class Foo {
    has @!array;
    method STORE(@!array) {
        say "STORED @!array[]";
        self
    }
}

my @b is Foo = 1,2,3;  # STORED 1 2 3

然而,当你真正开始使用这样一个数组时,你会面临一些奇怪的结果:

say @b[0]; # Foo.new
say @b[1]; # Index out of range. Is: 1, should be in 0..0

不谈这些结果的原因,应该清楚的是,要完全模仿一个Array,还需要很多东西。幸运的是,有一些生态系统模块可以帮助你解决这个问题。对于数组, 使用 Array::Agnostic 而对于散列, 使用 Hash::Agnostic

BUILD #

BUILD 方法的语义也发生了微妙的变化。在 Raku 中,BUILD 方法将作为对象方法被调用,并接收所有给 .new 的参数,之后它将完全负责初始化对象属性。当你使用内部助手模块 BUILDPLAN 时,这一点会变得更加明显。该模块显示了用默认的 .new 方法构建类的对象时,将对其进行的操作。

class Bar {
    has $.score = 42;
}
use BUILDPLAN Bar;
# class Bar BUILDPLAN:
#  0: nqp::getattr(obj,Foo,'$!score') = :$score if possible
#  1: nqp::getattr(obj,Foo,'$!score') = 42 if not set

这是内部的说法 - 把可选的命名参数 score 的值分配给 $!score 属性 - 把 42 的值分配给 $!score 属性,如果它还没有被设置的话。

现在,如果我们在类中添加一个 BUILD 方法,构建计划就会改变:

class Bar {
    has $.score = 42;
    method BUILD() { }
}
use BUILDPLAN Bar;
# class Bar BUILDPLAN:
#  0: call obj.BUILD
#  1: nqp::getattr(obj,Foo,'$!score') = 42 if not set

请注意,现在已经没有自动尝试取名为参数的 score 值了。这意味着,如果你有很多命名参数,而且只有一个参数需要特殊处理,你需要在你的自定义 BUILD 方法中做很多工作。这就是为什么要加入 TWEAK 方法的原因:

class Bar {
    has $.score = 42;
    method TWEAK() { }
}
use BUILDPLAN Bar;
# class Bar BUILDPLAN:
#  0: nqp::getattr(obj,Foo,'$!score') = :$score if possible
#  1: nqp::getattr(obj,Foo,'$!score') = 42 if not set
#  2: call obj.TWEAK

请注意,TWEAK 方法是在所有正常检查和初始化之后被调用的。这在大多数情况下是更有用的。

结束语 #

虽然真正的多态对象的想法已经在 Raku 中实现了,但结果却与最初的设想大相径庭。事后看来,我们可以看到为什么决定为所有对象支持一个不断增加的特殊方法列表是不切实际的。相反,我们选择了只实现提案中的几个关键方法,而对其他方法则采取了自动强制的方法。

原文: https://raku-advent.blog/2020/08/17/rfc-159-by-nathan-wiger-true-polymorphic-objects/