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?
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.
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.)
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
bar
to foo
so that foo
gets parameterizedWe'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
.
This line ensures our new parameterized type plays well with the system:
$type.^set_name: this.^name ~ '[' ~ T.^name ~ ']';
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With