A role is a standalone, named, reusable unit of behavior. You can compose a role into a class at compile time or add it to an individual object at runtime.
That's an abstract definition best explained by an example. This program demonstrates a simple and pluggable IRC bot framework which understands a few simple commands.
Previous chapters have explained classes and grammars. A role is another type of package. Like classes and grammars, a role can contain methods (including named regexes) and attributes. However, a role cannot stand on its own. To use a role, you must incorporate it into an object, a class, or a grammar.
In traditional object orientation, classes perform two tasks. They represent entities in the system by providing models from which to create instances. They also provide a mechanism for code re-use. These two tasks are somewhat in tension. For optimal re-use, classes should be small, but in order to represent a complex entity with many behaviors, classes tend to grow large.
Perl 6 classes retain the responsibility for instance management. Roles handle the task of code reuse. A role contains the methods and attributes required to provide a named, reusable unit of behavior. Building a class out of roles uses a safe mechanism called flattening composition. You may also apply a role to an individual object. Both of these design techniques appear in the example code.
Roles may also support parameters. This helps Perl 6 provide generic programming, along the lines of generics in C# and Java, or templates in C++.
Take a look at the KarmaKeeper
class definition. The body is empty; the class defines no attributes or methods of its own. The class inherits from IRCBot
, using the is
trait modifier -- something familiar from earlier chapters -- but it also uses the does
trait modifier to compose two roles into the class.
The process of role composition is simple. Perl takes the attributes and methods defined in each role and copies them into the class. After composition, the class appears as if it had defined those behaviors itself. This is part of the flattening property: after composing a role into the class, the roles in and of themselves are only important when querying the class to determine if it performs the role. Querying the methods of the KarmaKeeper
class through introspection will report that the class has both a process
method and an on-message
multi method.
If this were all that roles provided, they'd have few advantages over inheritance or mixins. Roles get much more interesting in the case of a conflict. Consider the class definition:
Both the AnswerToAll
and AnswerIfTalkedTo
roles provide a method named process
. Even though they share a name, the methods perform semantically different behaviors--behaviors which conflict. The role composer will produce a compile-time error about this conflict, asking the programmer to provide a resolution.
Multiple inheritance and mixin mechanisms rarely provide this degree of conflict resolution. In those situations, the order of inheritance or mixin decides which method wins. All possible roles are equal in role composition.
What can you do if there is a conflict? In this case, it makes little sense to compose both of the roles into a class. The programmer here has made a mistake and should choose to compose only one role to provide the desired behavior. An alternative way to resolve a conflict is to write a method with the same name in the class body itself:
If the role composer detects a method with the same name in the class body, it will then disregard all of the (possibly conflicting) ones from the roles. Put simply, methods in the class always win.
Sometimes it's okay to have multiple methods of the same name, provided they have different signatures such that the multidispatch mechanism can distinguish between them. Multi methods with the same name from different roles will not be in conflict. Instead, the candidates from all of the roles will combine together during composition into the class.
If the class provides a method of the same name that is also multi, then the multi candidates from the class will be included. On the other hand, if the class has a method of the same name that is not declared as a multi, then the method in the class alone--as usual--will win.
This is the mechanism by which a class that composes both, for example, the KarmaTracking
role and the Oping
role would end up having the candidates that both roles provide for the on-message
method. As a class ends up composing more than a couple of roles, it may be preferable to use an alternative syntax that allows you to list the roles in the class body:
This needs an index tag or two, but I'm not sure which.
The process
methods of the roles AnswerToAll
and AnswerIfTalkedTo
use a modified syntax for calling methods:
The use of .*
instead of .
changes the semantics of the method dispatch. Just as the *
quantifier in regexes means "zero or more", the .*
dispatch operator will call zero or more matching methods. If no on-message
multi candidates match, the call will not produce an error. If more than one on-message
multi candidate matches, Perl will call all of them, whether found by multiple dispatch, searching the inheritance hierarchy, or both.
There are two other variants. .+
greedily calls all methods but dies if there is not at least one method that matches. .?
, which tries to call one method, but returns a Failure
rather then throwing an exception. The zero-or-more and one-or-more dispatch forms are somewhat rare, but very useful in event driven programming. One-or-failure is very useful when dealing with per-object role application.
The role AnswerIfTalkedTo
declares a stub for the method bot-nick
, but never gives an implementation.
In the context of a role, this techniques declares that any class which composes this role must somehow provide a method named bot-nick
. The class itself may provide it, another role must provide it, or a parent class must provide it. IRCBot does the latter; it IRCBot defines an attribute $!bot-nick
along with an accessor method.
What about AUTOMETH
?
You are not required to make explicit the methods on which your role depends. If you do so, the role composer can detect any errors at compile time. If you do not, the problem will only appear at runtime, when and if something attempts to call the missing method.
I don't like the term "mixin" for this.
Class-based OO usually works, where instances have the methods and attributes of the classes they instantiate. Sometimes, something more dynamic is useful. Perl 6 allows you to add extra behaviors to individual objects by applying roles to individual objects at runtime.
The example in this chapter uses this in order to give bots new abilities during its lifetime. The Plugins
role is at the heart of this. Note the signature of the method on-message
. It captures the invocant into a variable $self
marked rw
, which indicates that the invocant may be modified. Inside the method, that happens:
The "first-class roles" mention needs to move out of a parenthetical note.
Like classes, roles are first-class in Perl 6; you can pass them around just like any other object. The %pluggables
hash maps names of plug-ins to Role objects. Thus this lookup stores a Role in $plug-in
. The does
operator adds this role to $self
--not the class of $self
, but the instance itself. From this point on, $self
now has all of the methods from the role, in addition to all of the ones that it had before. Importantly, this does not have any influence on any other instances of the same class; only this one instance has changed.
Runtime application differs from compile time composition in that methods in the applied role in will automatically override any of the same name within the class of the object. It's as if you had written an anonymous subclass of the current class of the object that composed the role into it. This means that .*
will find both those methods that mixed into the object from one or more roles along with any that already existed in the class.
If you wish to apply multiple roles at a time, list them all with does
. This case behaves the same way as compile-time composition, in that the role composer will compose them all into the imaginary anonymous subclass. As you might expect, any conflicts will occur at this point.
This gives a degree of safety, but it happens at runtime and is thus not as safe as compile time composition. For safety, perform your compositions at compile time. For example, instead of applying multiple roles to an instance, compose them into a new role at compile time and apply that role to the instance.
Hey! The above document had some coding errors, which are explained below:
- Around line 1:
-
Unknown directive: =head0
- Around line 245:
-
=end for without matching =begin. (Stack: [empty])
- Around line 293:
-
=end for without matching =begin. (Stack: [empty])
- Around line 306:
-
=end for without matching =begin. (Stack: [empty])
- Around line 334:
-
=end for without matching =begin. (Stack: [empty])