关于元编程:如何在Perl 6中将类设为参数化?

How can classes be made parametric in Perl 6?

通常在Perl 6中,只允许角色角色化。在这里,我们将尝试创建类,这种类(通常称为元对象)通常是不允许参数化的。

如果您尝试以天真的方式使类参数化,则会发生这种情况:

1
2
3
4
5
6
7
bastille% perl6 -e 'class Foo[::T] {}'
===SORRY!=== Error while compiling -e
Unable to parse class definition
at -e:1
------> class Foo?[::T] {}
    expecting any of:
        generic role

但是,如果您查看NativeCallCArray类型使用的元对象,您会发现它实际上是一个类,而不是角色,但是它仍然是参数!

1
2
bastille% perl6 -MNativeCall -e 'say CArray[int32].HOW.^name'
Perl6::Metamodel::ClassHOW+{}+{}

这是怎么做的?


使类参数化需要一些元编程来完成。可以像下面这样实现一个简单的参数化容器类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
use v6.d;

class Container {
    my role ContainerImpl[::T] {
        has T $.value;

        method new(Container: T $value) {
            self.bless: :$value
        }

        multi method gist(Container:D: --> Str:D) {
            $!value.gist
        }
        multi method Str (Container:D: --> Str:D) {
            $!value.Str
        }
        multi method perl(Container:D: --> Str:D) {
            self.^name ~ '.new(' ~ $!value.perl ~ ')'
        }
    }

    method ^parameterize(Mu:U \\this, Mu \\T) {
        my $type := this.^mixin: ContainerImpl[T];
        $type.^set_name: this.^name ~ '[' ~ T.^name ~ ']';
        $type
    }
}

say Container[Int].new(1).perl;
# OUTPUT: Container[Int].new(1)

那么这是如何工作的?

充当Perl6::Metamodel::MetaMethodContainer角色的

元类(例如Perl6::Metamodel::ClassHOW)可以将其他元方法与类型的专有技术混合在一起(该知识描述了特定类型的类型,例如类或角色的行为) 。解析类型名称时,Rakudo的语法会在使用参数类型和任何参数化类型作为参数的任何给定类型上调用parameterize元方法。通常,应该将参数化类型实现参数化原型,但是这里不做检查,只要实现parameterize元方法,就可以对任何类型的参数进行参数化。

mixin元方法特定于Perl6::Metamodel::Mixins角色,Perl6::Metamodel::ClassHOW也是如此。该方法通过重载类来混合角色,因此它被认为与传递给它的(在这种情况下为参数化的)角色具有相同的类型。

结合使用parameterizemixin元方法,您可以在角色中实现参数行为,然后通过在混合之前对其进行参数化来在类中使用它。这使类的行为就像实际上是参数化的一样。类型,即使从技术上讲它仍然不是一个。


TL; DR此答案是@Kaiepi的"简化"版本。它仅涵盖了从他们的答案中提取的以下所示的核心代码。编写该文件的目的是使其成为独立的解释,或者作为对它们答案的介绍或补充。

使类成为参数

名义上的问题非常广泛。但是问题的实质归结为使一个类参数化,而这正是这个答案(和@Kaiepi的答案)所关注的。

类作为一种类型,不支持现成的参数化。但是P6是完全可以元编程的。因此,您可以对一个类进行元编程以添加参数。注意这不是官方支持的技术!1

(您可以在种类级别添加参数性,以使所有类或从类派生的某种新类型都是参数性的。但是我认为这将花费大量的精力。2同时有六个使单个类参数化所需的全部工作都是相当简单的元编程。因此,这就是我们在此答案中要做的所有事情。)

编码

1
2
3
4
5
6
7
8
9
10
11
12
class foo {
    my role bar[::T] {}

    method ^parameterize(Mu:U \\this, Mu \\T) {
        my $type := this.^mixin: bar[T];
        $type.^set_name: this.^name ~ '[' ~ T.^name ~ ']';
        $type
    }
}

say foo[Int].new.perl;
# OUTPUT: foo[Int].new

上面的代码摘自@Kaiepi的答案,而忽略了我认为不必要的内容。此答案的其余部分详细解释了代码。

role bar[::T]

A role像类一样将属性和方法收集在一起。此SO上下文的主要区别在于角色是可参数化的,可以将其添加到类中,以便该类可以参数化。

[]之间的位是一个签名。 ::T是类型变量。签名可以像您想要的那样复杂,就像常规函数签名一样。

我已经显示过的bar角色有一个空的正文。在此技术的实际应用中,您将编写要添加到foo类的属性和方法。这些将是需要利用参数化的属性和方法,以及合理地包含在同一角色中的其他属性和方法。

^some-method-name

方法名称开头的

A ^表示它将不是对其显式调用方的调用,而是对调用方"最多"体现的"高阶工作"的调用在知道这种类型如何工作的专有技术对象中。

声明一个带有初始^的方法将导致自定义包含类的专有技术对象以包括该方法。

^parameterize

如果在编译器需要类型的地方编写foo[...],则编译器将调用(等效于)foo.^parameterize,这将转换为对foo \\ knowhow对象的parameterize的调用。

并且foo \\的专有技术对象已被定制为包括我们的方法:

1
2
3
4
5
method ^parameterize(Mu:U \\this, Mu \\T) {
    my $type := this.^mixin: bar[T];
    $type.^set_name: this.^name ~ '[' ~ T.^name ~ ']';
    $type
}

\\this

this的全部内容是什么? (\\只是表示"切下印记";我不是那个意思。)

thisfoo类型的对象,即在foo中的普通方法中与self关联的相同类型的对象,它们不是以^ .3开头的

bar添加到foo,以便对foo进行参数化

我们现在已经可以生成参数化的foo

1
    my $type := this.^mixin: bar[T];

从保存在this中的未参数化foo开始,我们将bar中的" mix "参数化,并将T传递给^parameterize

遵循P6标称类型系统的协议

此行确保我们的新参数化类型在系统中正常运行:

1
    $type.^set_name: this.^name ~ '[' ~ T.^name ~ ']';

转到@Kaiepi的答案

此答案是@Kaiepi答案的简化版本。

如果实际的实现是带有参数化公共属性的类,则仅仅涵盖确保.perl正常工作之类的问题还不够。

脚注

1元模型的许多细节都不是官方P6的一部分。 .^parameterize方法不是。

2我非常有信心,通过适当的(了解胆量和元数据编程),一个人可以使所有类或从类派生的一种新类型的行为像角色一样,因为它是一种支持参数化的类型\\使用明显的语法"开箱即用":

1
class foo[::T] { ... }

3我非常赞同@Kaiepi的决定,不使用\\self作为^方法的第一个参数。这将是通常self的谎言和阴影。大概@Kaiepi的想法是this通常被用作self的同义词,但是,如果您知道P6,则显然与self不同,因为它是第一个参数,但不是invocant参数。