Wait the light to fall

接口多态性被认为是可爱的

焉知非鱼

先说点题外话。在写这篇文章的过程中,我遭遇了系统管理员最可怕的噩梦:服务器丢失,然后是备份不好。直到最后一刻,我都有充分的理由担心自己根本无法完成这篇文章。幸运的是,我与生命做了休战,得到了暂时的喘息。结论是什么?不要在 ESXi 上使用 bareos。或者,可能就是不要使用裸 OS…

在为我之前的 advent 日志领取 RFC 的时候,我完全把注意力放在了语言对象部分。我花了几遍时间才找到合适的内容。但在这期间,我发现列表中居然少了一个非常重要的主题。“不可能!” - 我自言自语道,随后又继续搜索。然而,无论是搜索"抽象类",还是搜索"角色",都没有任何结果。我本想放弃,并做出结论,这个想法是后来才有的,当时写提纲的时候还是这样的。

但是,等等,OO 相关的 RFC 中提到的主题是什么接口?哦,那个接口! 就像请求正文中所说的那样。

增加一个机制来声明类的接口 还有一个方法来声明一个类实现了上述接口。

此时我再次意识到,现在已经落后了整整20年。这段文字来自于很多人认为 Java 是唯一正确的 OO 实现的时代! 而事实上,通过进一步的阅读,我们发现下面的说法,很可能是受到当时一些流行观点的影响。

现在,如果一个接口文件试图做任何事情,而不是预先声明方法,这都是一个编译时错误。

这让人想起了什么,不是吗?然后,在 RFC 的最后,我们又发现了一个问题。

Java 是一种使用接口多态性的语言。不要被它吓倒–如果我们一定要从 Java 中偷点东西,那就偷点好东西吧。

好东西?好东西?哦,我的……Java 试图通过简单地否认 C++ 多重继承方式来解决它的问题,这是让我从一开始就远离这门语言的原因。早在90年代初,我就受够了 Pascal 控制我的写作风格!

幸运的是,那些参与早期 Perl6 设计的人一定和我一样对这个问题有相同的看法(此外,Java 本身也发生了很大的变化)。所以,我们现在有了角色。它们与抽象类和现代接口的共同点是,一个角色可以定义一个接口来与一个类进行通信,也可以提供一些特定角色行为的实现。它还可以做得更多一些,不仅仅是这些!

角色的不同之处在于 Raku OO 模型中角色的使用方式。一个类不会实现一个角色;也不会像抽象类那样从它那里继承。相反,它做的是角色;或者我喜欢用另一个词来形容:它消耗一个角色。从技术上讲,这意味着角色被混入类中。这个过程可以形象地描述为:如果编译器将角色的类型对象所包含的所有方法和属性,重新植入到类中。类似这样。

role Foo {
    has $.foo = 42;
    method bar {
        say "hello!"
    }
}
class Bar does Foo { }
my $obj = Bar.new;
say $obj.foo; # 42
$obj.bar;     # hello!

它和继承有什么不同呢?我们把 Bar 这个类改一下:

class Baz {
    method bar {
        say "hello from Baz!"
    }
}
class Bar does Foo is Baz {
    method bar {
        say "hello from Bar!";
        nextsame
    }
}
Bar.new.bar; # hello from Bar!
             # hello from Baz!

nextsame 在这种情况下,将一个方法调用重新分配给继承层次中下一个同名方法。简单地说,它将控制权传递给了 Baz::bar 方法,从我们收到的输出中可以看出。而 Foo::bar 呢?它不在那里。当编译器将角色混入 Bar 时,它发现这个类确实已经有了一个名为 bar 的方法。因此,Foo 的那个方法被忽略了。由于 nextsame 只考虑继承层次中的类,所以 Foo::bar 不会被调用。

通过另一个技巧,也可以明确与接口消耗的区别:

class Bar {
    method bar {
        say "hello from Bar!"
    }
}
my $obj = Bar.new;
$obj.bar; # hello from Bar!
$obj does Foo;
$obj.bar; # hello!

在这个例子中,角色被混合到一个退出的对象中,这要归功于 Raku 的动态特性,它使之成为可能。当一个角色以这种方式被应用时,它的内容就会在类的内容上被强制执行,类似于病毒将它的遗传物质注入到细胞中,有效地覆盖了内部进程。这就是为什么第二次对 bar 的调用会被派发到 Foo::bar 方法,而 Bar::bar 这次在 $obj 上找不到的原因。

为了把这个问题完全讲清楚,我给大家看一些有趣的代码例子。其中使用的操作符 but 的行为就像 do 一样,只是它并没有修改它的 LHS 对象,而是创建并返回一个新的对象:

‌‌my $s1 = "not empty means true";
my $s2 = $s1 but role { method Bool { False } };
say $s1 ?? "true" !! "false";
say $s2 ?? "true" !! "false";

这个片段我留给你自己去尝试,因为我的文章该转到另一个话题了:角色参数化。

考虑一下这个例子:

role R[Str:D $desc] {
    has Str:D $.description = $desc;
}
class Foo does R["some info"] { }
say Foo.new.description; # some info

或者更实际的一个例子:

role R[::T] {
    has T $.val is rw;
}
class ContInt does R[Int] { }
ContInt.new.val = "oops!"; # "Type check failed..." exception is thrown

后面的例子利用了所谓的类型捕获,其中 T 是一个通用类型,这个概念很多人可能从其他语言中知道,只有当角色被消耗并提供参数时,它才会变成一个具体的类型,就像类 ContInt 声明一样。

今天我要介绍的参数的最后一个迭代,就是这个比较广泛的例子:

role Vect[::TX] {
    has TX $.x;
    method distance(Vect $v) { ($v.x - $.x).abs }
}
role Vect[::TX, ::TY] {
    has TX $.x;
    has TY $.y;
    method distance(Vect $v) { 
        (($v.x - $.x)² + ($v.y - $.y)²).sqrt 
    }
}

class Foo1  does Vect[Rat]      { }
class Foo2 does Vect[Int, Int] { }

my $foo1 = Foo1.new(:x(10.0));
my $foo2 = Foo2.new(:x(10), :y(5));
say $foo1;                                   # Foo1.new(x => 10.0)
say $foo2;                                   # Foo2.new(x => 10, y => 5)
say $foo2.distance(Foo2.new(:x(11), :y(4))); # 1.4142135623730951

希望这段代码能解释清楚。最为肯定的是,它很好地直观地展示了语言设计者们自最初的 RFC 制作以来所走过的漫长道路。

最后,我想分享一些关于 Raku 角色和它们在 Rakudo 中的实现的有趣事实。

  1. 从 Raku v6.e 开始,一个角色可以定义自己的构造函数/析构子方法。它们并不像方法那样被混入一个类中。相反,它们被用来构建/销毁一个对象,就像类的构造函数/破坏函数一样。
use v6.e.PREVIEW; # 6.e is not released yet
role R { submethod TWEAK { say "R" } }
class Foo { submethod TWEAK { say "Foo" } }
class Bar is Foo does R { submethod TWEAK { say "Bar" } }
Bar.new; # Foo
         # R
         # Bar
  1. 角色体是一个子程序。试试这个例子:
role R { say "Role" }
class Foo { say "Foo" }
# Foo

然后修改类 Foo,使其消耗 R。

class Foo does R { say "Foo" }
# Role
# Foo

输出的不同之处在于,当角色本身混合到一个类中时,角色体会被调用。试着在使用 Foo 的同时再增加一个消耗 R 的类,看看输出如何变化。为了让类和角色体之间的区别更加清晰,让你的新类继承自 Foo。即使是和看起来很像,但它们的行为却有很大的不同。

  1. 角色声明中的方括号包含一个签名。事实上,它是角色体子程序的签名! 这使得一些非常有用的技巧成为可能。
# Limit role parameters to concrete numeric objects.
role R[Numeric:D ::T $default] {
    has T $.value = $default;
}
class Foo[42.13] { };
say Foo.new.x; # 42.13
Or even:

# Same as above but only allow specific values.
role R[Numeric:D ::T $default where * > 10] {
    has T $.value = $default;
}

此外,在为一个角色声明了几个不同的参数候选者时,选择合适的候选者是一个与选择几个 multi 候选者的合适例程相同的任务,并且基于与传递的参数的匹配签名。

  1. Rakudo用四种不同的角色类型来实现一个角色! 让我根据前面事实的例子,用下面的片段来演示一个方面。
for Foo.^roles -> \consumed {
    say R === consumed
}

=== 是一个严格的对象标识运算符。在我们的情况下,我们可以把它看作是一个严格的类型等价运算符,它告诉我们两个类型是否真的是完全相同的一个类型。

而我希望以后会有一篇更广泛的文章来介绍这个主题,此时我只提供上面片段的输出作为提示,使其成为一个经典的突兀的开放性结局:

False