Raku 建立在元对象层之上。这意味着存在控制各种面向对象构造(如类、角色、方法、属性或枚举)行为的对象(即 *元对象*)。

当需要普通对象的类型时,元对象对用户具有实际意义。例如

my $arr = [12];
say $arr.^name;   # OUTPUT: «Array␤» 

要更深入地了解 class 的元对象,以下是一个示例,重复两次:一次以 Raku 中的普通声明形式,一次以 元模型 表达的形式

class A {
    method x() { say 42 }
}
 
A.x();

对应于

constant A := Metamodel::ClassHOW.new_typename => 'A' );  # class A { 
A.^add_method('x'my method x(A:{ say 42 });             #   method x() 
A.^compose;                                                 # } 
 
A.x();

(除了声明形式在编译时执行,而后面形式不执行)。

可以使用 $obj.HOW 获取对象背后的元对象,其中 HOW 代表更高阶的工作方式(或者,*这个东西是怎么工作的?*)。

这里,使用 .^ 的调用是针对元对象的调用,因此 A.^composeA.HOW.compose(A) 的简写。调用者也包含在参数列表中,以使支持原型风格的类型系统成为可能,在原型风格的类型系统中,只有一个元对象(而不是每个类型一个元对象,就像标准 Raku 一样)。

如上例所示,所有面向对象特性都可供用户使用,而不仅仅是编译器。实际上,编译器只是使用对元对象的此类调用。

元方法§

这些是类似于方法调用的内省宏。

元方法通常以全大写命名,避免创建自己的全大写名称的方法被认为是良好的风格(因为它们通常用于诸如 Phaser 之类的东西)。这将避免与未来语言版本中可能出现的任何元方法发生冲突。

WHAT§

类型的类型对象。这是一种伪方法,可以在不产生错误或警告的情况下重载,但会被忽略。

例如,42.WHAT 返回 Int 类型对象。

WHICH§

对象的标识值。这可用于哈希和标识比较,也是 === 中缀运算符的实现方式。

WHO§

支持该对象的包。

WHERE§

对象的内存地址。请注意,这在具有移动/压缩垃圾收集器的实现中并不稳定。使用 WHICH 作为稳定的标识指示器。

HOW§

返回元类对象,如“高阶工作”中所示。

say (%).HOW.^name # OUTPUT: «Perl6::Metamodel::ClassHOW+{<anon>}␤»

HOW 在这种情况下返回类型为 Perl6::Metamodel::ClassHOW 的对象;此类型的对象用于构建类。对 & 符号的相同操作将返回 Perl6::Metamodel::ParametricRoleGroupHOW。每当使用 ^ 语法访问元方法时,您都会调用此对象。实际上,上面的代码等效于 say (&).HOW.HOW.name(&),这要笨拙得多。 Metamodel::ClassHOW 是 Rakudo 实现的一部分,因此请谨慎使用。

WHY§

附加的 Pod 值。

DEFINITE§

该对象具有有效的具体表示。这是一种伪方法,可以在不产生错误或警告的情况下重载,但会被忽略。

对于实例返回 True,对于类型对象返回 False

VAR§

返回底层的 Scalar 对象(如果有)。

Scalar 对象的存在表明该对象是“项目化的”。

.say for (123);           # OUTPUT: «1␤2␤3␤», not itemized 
.say for $(123);          # OUTPUT: «(1 2 3)␤», itemized 
say (123).VAR ~~ Scalar;  # OUTPUT: «False␤» 
say $(123).VAR ~~ Scalar# OUTPUT: «True␤»

有关更多信息,请参阅 项目上下文部分

元类方法§

与您可以定义对象和类方法(它们无法访问实例变量)一样,您可以定义元类方法,这些方法将在元类上运行。这些通常由方法标识符前面的插入符号 (^) 定义。这些元类方法可能会返回类型对象或简单对象;一般来说,它们只是与元对象协议相关,并且是具有特殊语法的简单方法。

这些方法将使用类型名称作为第一个参数调用,但需要明确声明。

class Foo {
    method ^barMu \foo{
        foo.^set_namefoo.^name ~ "[þ]" );
    }
}
my $foo = Foo.new();
say $foo.^name# OUTPUT: «Foo␤» 
Foo.^bar();
say $foo.^name# OUTPUT: «Foo[þ]␤» 

此元类方法将通过调用类元方法来更改其声明的类的名称。由于它一直在元类上运行,因此同一类的任何新对象都将接收相同的名称;调用 say Foo .new().^name 将返回相同的值。如您所见,元类方法在没有参数的情况下被调用;在这种情况下,\foo 在被调用时将变为 Foo

元类方法可以接收任意数量的参数。

class Foo {
    method ^barMu \fooStr $addenda{
        foo.^set_namefoo.^name ~ $addenda );
    }
}
Foo.new().^bar(  "[baz]" );
my $foo = Foo.new();
say $foo.^name;  # OUTPUT: «Foo[baz]␤» 

再次强调,隐式地,方法调用将提供第一个参数,即类型对象。由于它们是元类方法,因此可以在类(如上所示)或对象(如下所示)上调用它们。结果将完全相同。

元对象系统结构§

注意: 此文档主要反映了 Rakudo Raku 编译器 实现的元对象系统,因为 设计文档 对细节的描述非常少。

对于每个类型声明关键字,例如 classroleenummodulepackagegrammarsubset,在 Metamodel:: 命名空间中都有一个单独的元类。(Rakudo 在 Perl6::Metamodel:: 命名空间中实现它们,然后将 Perl6::Metamodel 映射到 Metamodel)。

许多这些元类共享通用功能。例如,角色、语法和类都可以包含方法和属性,以及能够执行角色。此共享功能在角色中实现,这些角色被组合到相应的元类中。例如,角色 Metamodel::RoleContainer 实现了一种类型可以保存角色的功能,而 Metamodel::ClassHOW(它是 class 关键字背后的元类)则执行此角色。

大多数元类都有一个 compose 方法,您在创建或修改元对象后必须调用它。它创建方法缓存,验证内容等等,如果您忘记调用它,就会出现奇怪的行为,所以不要忘记 :-)。

引导问题§

您可能想知道 Metamodel::ClassHOW 如何成为一个类,当类是根据 Metamodel::ClassHOW 定义的,或者负责角色处理的角色如何成为角色。答案是通过魔法

只是开玩笑。引导是特定于实现的。Rakudo 通过使用它本身实现的语言的对象系统来实现它,该语言恰好是 (几乎) Raku 的一个子集,称为 NQP。NQP 有一种原始的、类似的类型,称为 knowhow,它用于引导它自己的类和角色实现。knowhow 建立在 NQP 下的虚拟机提供的原语之上。

由于对象模型是根据更低级别的类型引导的,因此内省有时会返回低级别类型而不是您期望的类型,例如 NQP 级别的例程而不是正常的 Routine 对象,或者引导属性而不是 Attribute

组合时间和静态推理§

在 Raku 中,类型是在解析时构建的,因此在开始时,它必须是可变的。但是,如果所有类型始终是可变的,那么对它们的任何推理都会在类型修改时失效。例如,父类型列表以及类型检查的结果可能会在此期间发生变化。

因此,为了兼顾两者的优势,有一个时间点,类型从可变转变为不可变。这称为组合,对于语法上声明的类型,它发生在类型声明完全解析时(因此通常在解析结束的花括号时)。

如果您直接通过元对象系统创建类型,则必须在它们变得完全功能之前在它们上调用 .^compose

大多数元类还使用组合时间来计算一些属性,例如方法解析顺序,发布方法缓存以及其他内部管理任务。在类型组合后干预它们有时是可能的,但通常会导致灾难。不要这样做。

权力与责任§

元对象协议提供了普通 Raku 代码有意限制的许多功能,例如在不信任您的类上调用私有方法,窥视私有属性,以及其他通常不会执行的操作。

普通 Raku 代码有许多安全检查;元模型则没有。它靠近底层的虚拟机,违反与 VM 的契约会导致各种奇怪的行为,这些行为在普通代码中显然是错误。

因此,在编写元类型时要格外小心和周到。

权力、便利和陷阱§

元对象协议旨在足够强大,可以实现 Raku 对象系统。这种强大功能有时会以便利性为代价。

例如,当你写 my $x = 42 然后继续对 $x 调用方法时,大多数这些方法最终会作用于 整数 42,而不是它存储的 标量容器。这是普通 Raku 中的一种便利功能。元对象协议的许多部分无法负担自动忽略标量容器的便利性,因为它们也用于实现这些标量容器。因此,如果你写 my $t = MyType; ... ; $t.^compose,你正在组合 $-sigiled 变量隐含的标量,而不是 MyType

结果是,你需要对 Raku 的细微之处有相当详细的理解,才能在使用 MOP 时避免陷阱,并且不能期望普通 Raku 代码提供的相同 "按我的意思做" 便利性。

原型§

通常,当多种类型的类型共享一个属性时,它会使用一个元角色来实现,并将其混合到它们的元类中。但是,并非所有类型的常见属性都可以作为 mixin 实现。某些属性对于各种类型的类型是通用的,但没有足够的共享行为来作为 mixin 实现。这些属性被称为原型

HOW 应该提供一个 archetypes 元方法,该方法不接受任何参数并返回一个 Metamodel::Archetypes 实例。编译器使用它来确定元对象支持哪些原型。本节的其余部分将介绍 Rakudo 中存在的每个原型的工作原理。

参数化§

参数化类型是不完整的类型,可能具有任意数量的类型参数。这里,类型参数指的是类型本身的参数;这些可以是签名允许你包含的任何对象,而不仅仅是类型本身。当用类型参数参数化时,参数化类型将产生某种更完整的类型。

如果一个 HOW 支持参数化,它应该具有 parametric 原型,并且必须提供一个 parameterize 元方法。parameterize 元方法必须接受一个元对象,并且可以接受任意数量的类型参数作为参数,返回一个元对象。例如,允许类型用任何类型参数参数化的 parameterize 元方法可能具有以下签名

method parameterize(Mu $obj is raw|parameters --> Mu)

参数化类和语法§

由于参数化原型的实现方式,类和语法可以通过为它们提供 parameterize 元方法来增强对参数化的支持,尽管类型没有 parametric 原型。这在角色的功能使其不适合用作参数化类型的情况下很有用。

参数化类和语法有用的一个场景是,当类型的参数化应该覆盖或添加对原始参数化类型上的现有方法或正则表达式的多个分派候选者时。对于像 ArrayHash 这样的类型的参数化,情况就是这样,它们可以选择参数化以混合更类型安全的版本,这些版本直接使用实例的值。在 Rakudo 中,这些是使用 Metamodel::MixinsMetamodel::Naming 提供的元方法作为参数化类实现的,以创建给定元对象的 mixin 并重置其名称,然后再返回它。例如,当与多标记结合使用时,此技术可用于编写可扩展的语法

grammar Bot::Grammar {
    token TOP { <topic> || .+ }
 
    proto token topic {*}
    multi token topic:sym<command> { <command> <.ws> <command-args> }
 
    token command      { '$' <!ws>+ }
    token command-args { <!ws>+ % <.ws> }
 
    method ^parameterize(::?CLASS:U $this is raw+roles{
        my Str:D $name   = self.name: $this;
        my Mu    $mixin := $this.^mixin: |roles;
        $mixin.^set_name: [~$name'['roles.map(*.^name).join(','), ']';
        $mixin
    }
}
 
role Greetings[Str:D $name{
    multi token topic:sym<greeting> { ^ [ 'hi' | 'hello' | 'hey' | 'sup' ] <.ws> $name }
}
 
my constant GreetBot = Bot::Grammar[Greetings['GreetBot']];
GreetBot.parse: 'sup GreetBot';
say ~$/# OUTPUT: «sup GreetBot␤» 

参数化类还可以用于模拟对其他类型的参数化支持。例如,Failable 生态系统模块是一个参数化类,它在参数化时会生成一个子集。虽然模块本身会进行一些缓存以确保不会生成比必要更多的类型对象,但它的更基本版本可以这样实现

class Failable {
    method ^parameterize(Failable:U $this is rawMu $obj is raw --> Mu{
        Metamodel::SubsetHOW.new_type:
            name       => $this.^name ~ '[' ~ $obj.^name ~ ']',
            refinee    => Metamodel::Primitives.is_type($objAny?? Any !! Mu,
            refinement => $obj | Failure
    }
}