How can classes be made parametric in Perl 6?

旧街凉风 提交于 2020-01-12 15:46:05

问题


Normally in Perl 6, only roles are allowed to be parametric. Here, we'll be attempting to make classes, a kind (referred to from here on out as a metaobject) that isn't normally allowed to be parametric, parametric.

If you try to make a class parametric the naive way, this happens:

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

But if you take a look at what metaobject the CArray type from NativeCall uses, you'll find that it's in fact a class, not a role, yet it's still parametric!

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

How is this done?


回答1:


Making classes parametric takes a little bit of metaprogramming to accomplish. A simple parametric container class can be implemented like so:

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)

So how does this work?

Metaclasses that do the Perl6::Metamodel::MetaMethodContainer role, such as Perl6::Metamodel::ClassHOW, can have additional metamethods mixed in with the type's knowhow (which describes how a specific kind of type, such as a class or role, behaves). Rakudo's grammar invokes the parameterize metamethod on any given type with the parametric type and any parameterized types as arguments when parsing a type's name. Normally, types that are parametric are supposed to implement the parametric archetype, but this doesn't get checked here, which allows any type to be parameterized as long as it implements a parameterize metamethod.

The mixin metamethod is specific to the Perl6::Metamodel::Mixins role, which Perl6::Metamodel::ClassHOW also does. The method mixes in a role by reblessing the class so it's considered to be the same type as the (parameterized in this case) role passed to it.

Combining the parameterize and mixin metamethods allows you to implement parametric behaviour in a role, then use it in a class by parameterizing it before mixing it in. This allows the class to behave as if it actually were a parametric type, even though it's still technically not one.




回答2:


TL;DR This answer is a "simplified" version of @Kaiepi++'s. It only covers the core bit of code shown below that's extracted from their answer. It's written so that it should work as a standalone explanation, or as an introduction or complement to their answer.

Making a class parametric

The titular question is very broad. But the body of the question boils down to making a class parametric and that's what this answer (and @Kaiepi's) focuses on.

Classes, as a kind of type, don't support parametricity out of the box. But P6 is fully metaprogrammable. So you can just metaprogram a class to add parametricity. NB. This is not an officially supported technique!1

(You could add parametricity at the kind level, such that either all classes, or some new kind of type that you derive from classes, are parametric. But I think that would take considerable effort.2 In the meantime a half dozen lines of fairly straight-forward metaprogramming is all that's required to make a single class parametric. So that's all we'll do in this answer.)

The code

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

The above code is extracted from @Kaiepi's answer, leaving out what I considered non-essential. The rest of this answer explains the code in detail.

role bar[::T]

A role collects attributes and methods together just like a class. The key difference in the context of this SO is that a role is parameterizable and can be added to a class so that the class becomes parameterized.

The bit between the [ and ] is a signature. The ::T is a type variable. The signature can be as complex as you want it to be, just like a regular function signature.

The bar role I've shown has an empty body. In an actual application of this technique you would write the attributes and methods that you want added to the foo class. These would be attributes and methods that need to make use of the parameterization, plus other attributes and methods that it's reasonable to include in the same role.

^some-method-name

A ^ at the start of a method name signals that it will not be a call on its explicit invocant but rather a call "up to" the invocant's "higher order workings" as embodied in a knowhow object that knows how that kind of type works.

Declaring a method with an initial ^ causes the knowhow object for the containing class to be customized to include that method.

^parameterize

If you write foo[...] where the compiler expects a type, the compiler calls (the equivalent of) foo.^parameterize which turns into a call to parameterize on foo's knowhow object.

And foo's knowhow object has been customized to include our method:

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

\this

What's this all about? (The \ just means "slash the sigil"; I don't mean that aspect.)

this is the foo type object, i.e. the same type object associated with self in ordinary methods in foo that don't start with ^.3

Adding bar to foo so that foo gets parameterized

We've now arrived at the point where we can generate a parameterized foo:

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

Starting with an unparameterized foo held in this we "mix" in bar parameterized with the T passed to ^parameterize.

Following protocol for P6's nominal type system

This line ensures our new parameterized type plays well with the system:

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

Moving on to @Kaiepi's answer

This answer is a simplified version of @Kaiepi's answer.

It isn't sufficient to cover issues such as ensuring that .perl works correctly if an actual implementation is a class with parameterized public attributes.

Footnotes

1 Many details of the metamodel are not part of official P6. The .^parameterize method is not.

2 I'm pretty confident that, with suitable (learning about guts and) metaprogramming, one could make all classes, or a new kind derived from classes, behave like roles inasmuch as being a kind of type that supports parameterization "out of the box" using the obvious syntax:

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

3 I strongly concur with @Kaiepi's decision not to use \self as the first parameter of a ^ method. That would be a lie and shadow the usual self. Presumably @Kaiepi's thinking is that this is often used as a synonym of self but, if you know P6, clearly isn't the same as self because it's the first parameter but not the invocant parameter.



来源:https://stackoverflow.com/questions/57554660/how-can-classes-be-made-parametric-in-perl-6

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!