Apocalypse 12
by Larry Wall
|
Pages: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
Editor's Note: this Apocalypse is out of date and remains here for historic reasons. See Synopsis 12 for the latest information.
Declaration of Roles
A role is declared much like a class, but with a role keyword
instead:
role Pet {
method feed ($food) {
$food.open_can();
$food.put_in_bowl();
.call();
}
}
A role may not inherit from a class. It may be composed of other roles,
however. In essence, a role doesn't know its own type yet, because it
will be composed into another type. So if you happen to make any mention
of its main type (available as ::_), that mention is in
fact generic. Therefore the type of $self is generic. Likewise
if you refer to SUPER, the role doesn't know what the parent
classes are yet, so that's also generic. The actual types are instantiated
from the generic types when the role is composed into the class. (You
can use the role name ("Pet") directly, but only in places
where a role name is allowed as a type constraint, not in places that
declare the type of an actual object.)
Just as the body of a class declaration is actually a method call on
an instance of the MetaClass class, so too the body of
a role declaration is actually a method call on an instance of the MetaRole
class, which is like the MetaClass class, with some tweaks
to manage Role objects instead of Class objects.
For instance, a Role object doesn't actually support a
dispatcher like a Class object.
MetaRole and MetaClass do not inherit from
each other. More likely they both inherit from MetaModule
or some such.
Parametric types
A role's main type is generic by default, but you can also parameterize other types explicitly:
role Pet[Type $petfood = TableScraps] {
method feed (::($petfood) $food) {...}
}
Unlike certain other languages you may be altogether too familiar with, Perl uses square brackets for parametric types rather than angles. Within those square brackets it uses standard signature notation, so you can also use the arguments to pass initial values, for instance. Just bear in mind that by default any parameters to a role or class are considered part of the name of the class when instantiated. Inasmuch as instantiated type names are reminiscent of multimethod "long names", you may use a colon to separate those arguments that are to be considered part of the name from those that are just options.
Please note that these types can be as latent (or as non-latent) as you like. Remember that what looks like compile time to you is actually runtime to the compiler, so it's free to bind types as early or late as you tell it to, including not at all.
Interfaces
If a role merely declares methods without defining them, it degenerates to an interface:
role Pet {
method feed ($food) {...}
method groom () {...}
method scratch (+$where) {...}
}
When such a role is included in a class, the methods then have to be defined by the class that uses the role. Actually, each method is on its own--a role is free to define default implementations for any subset of the methods it declares.
Private interfaces
If a role declares private accessors, those accessors are private to the class, not the role. The class must define any private implementations that are not supplied by the role, just as with public methods. But private method names are never visible outside the class (except to its trusted proxy classes).
Encapsulated attributes
Unlike in the Traits paper, we allow roles to have state. Which is fancy way of saying that the role can define attributes, and methods that act on those attributes, not just methods that act only on other methods.
role Pet {
has $.collar = { Collar.new(Tag.new) };
method id () { return $.collar.tag }
method lose_collar () { undef $.collar }
}
By the way, I think that when $.collar is undefined, calling
.tag on it should merely return undef rather
than throwing an exception (in the same way that @foo[$x][$y][$z]
returns undef when @foo[$x] is undefined,
and for the same reason). The undef object returned should,
of course, contain an unthrown exception documenting the problem, so
that if the undef is ever asked to provide a defined value,
it can explain why it can't do so. Or if the returned value is tested
by //, it can participate in the resulting error message.
If you want to parameterize the initial value of a role attribute, be sure to put a colon if you don't want the parameter to be considered part of the long name:
role Pet[IDholder $id: $tag] {
has IDholder $.collar .= new($tag);
}
class Dog does Pet[Collar, DogLicense("fido")] {...}
class Pigeon does Pet[LegBand, RacerId()] {...}
my $dog = new Dog;
my $pigeon = new Pigeon;
In which case the long names of the roles in question are Pet[Collar]
and Pet[LegBand]. In which case all of these are true:
$dog.does(Dog)
$dog.does(Pet)
$dog.does(Pet[Collar])
but this is false:
$dog.does(Pet[LegBand])
Anyway, where were we. Ah, yes, encapsulated attributes, which leads us to...
Encapsulated private attributes
We can also have private attributes:
has Nose $:sniffer .= new();
And encapsulated private attributes lead us to...
Encapsulated delegation
A role can abstract the decision to delegate:
role Pet {
has $:groomer handles «bathe groom trim» = hire_groomer();
}
Now when the Dog or Cat class incorporates
the Pet role, it doesn't even have to know that the .groom
method is delegated to a professional groomer. (See section on Delegation
below.)
Encapsulated inheritance
It gets worse. Since you can specify inheritance with an "is" declaration within a class, you can do the same with a role:
role Pet {
is Friend;
}
Note carefully that this is not claiming that a Pet ISA
Friend (though that might be true enough). Roles never
inherit. So this is only saying that whatever animal takes on the role
of Pet gets some methods from Friend that
just happen to be implemented by inheritance rather than by composition.
Probably Friend should have been written as a
role, but it wasn't (perhaps because it was written in Some Other Language
that runs on Parrot), and now you want to pretend that it was
written as a role to get your project out the door. You don't want to
use delegation because there's only one animal involved, and inheritance
will work good enough till you can rewrite Friend in a
language that supports role playing.
Of course, the really funny thing is that if you go across a language barrier like that, Perl might just decide to emulate the inheritance with delegation anyway. But that should be transparent to you. And if two languages manage to unify their object models within the Parrot engine, you don't want to suddenly have to rewrite your roles and classes.
And the really, really funny thing is that Parrot implements roles internally with a funny form of multiple inheritance anyway...
Ain't abstraction wonderful.
Use of Roles at Compile Time
Roles are most useful at compile time, or more precisely, at class composition
time, the moment in which the MetaClass class is figuring
out how to put together your Class object. Essentially,
that's while the closure associated with your class is being executed,
with a little extra happening before and after.
A class incorporates a role with the verb "does", like this:
class Dog is Mammal does Pet does Sentry {...}
or equivalently, within the body of the class closure:
class Dog {
is Mammal;
does Pet;
does Sentry;
...
}
There is no ordering dependency among the roles, so it doesn't matter
above if Sentry comes before Pet. That is
because the class just remembers all the roles and then meshes them
after the closure is done executing.
Each role's methods are incorporated into the class unless there is already a method of that name defined in the class itself. A class's method definition hides any role definition of the same name, so role methods are second-class citizens. On the other hand, role methods are still part of the class itself, so they hide any methods inherited from other classes, which makes ordinary inherited methods third-class citizens, as it were.
If there are no method name conflicts between roles (or with the class), then each role's methods can be installed in the class, and we're done. (Unless we wish to do further analysis of role interrelationships to make sure that each role can find the methods it depends on, in which case we can do that. But for 6.0.0 I'll be happy if non-existent methods just fail at runtime as they do now in Perl 5.)
If, however, two roles try to introduce a method of the same name (for some definition of name), then the composition of the class fails, and the compilation of the program blows sky high--we sincerely hope. It's much better to catch this kind of error at compile time if you can. And in this case, you can.
Conflict resolution
There are several ways to solve conflicts. The first is simply to write a class method that overrides the conflicting role methods, perhaps figuring out which role method to call. It is allowed to use the role name to select one of the hidden role methods:
method shake ($self: $arg) {
given $arg {
when Culprit { $self.Sentry::shake($arg) }
when Paw { $self.Pet::shake($arg) }
}
}
So even though the methods were not officially composed into the class, they're still there--they're not thrown away.
That last example looks an awful lot like multiple dispatch, and in fact,
if you declare the roles' methods with multi, they would
be treated as methods with different "long names", provided their signatures
were sufficiently different.
An interesting question, though, is whether the class can force two role methods that weren't declared "multi" to behave as if they were. Perhaps this can be forced if the class declares a signatureless multi stub without defining it later in the class:
multi shake {...}
The Traits paper recommends providing ways of renaming or excluding one or the other of the conflicting methods. We don't recommend that, because it's better if you can keep both contracts through multiple dispatch to the role methods. However, you can force renaming or exclusion by pretending the role is a delegation:
does Pet handles [ :myshake«shake», Any ];
does Pet handles { $^name !~ "shake" };
Or something that. (See the section on Delegation below.) If we can't get that to work right, you can always say something like:
method shake { .Sentry::shake(@_) } # exclude Pet::shake
method handshake { .Pet::shake(@_) } # rename Pet::shake
In many ways that's clearer than trying to attach a selection syntax to "does".
Pages: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 |

