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.